diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 02aeed8..e03bb1a 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -63,3 +63,47 @@ jobs: tags: | ${{ steps.image.outputs.name }}:latest ${{ steps.image.outputs.name }}:${{ github.sha }} + + # Deploy jobs trigger a Render deploy hook pinned to the image tag that was + # just published (see docs/deployment.md for the one-time Render setup). + # The hook URL is a secret; when it isn't configured the step no-ops (exits + # 0 without deploying), so this workflow is safe to merge ahead of that + # setup. Note: the `secrets` context is NOT available in a job-level `if:`, + # so the presence check is done in the step's shell, not the `if:`. + deploy-server: + name: Deploy server to Render + needs: build-server + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Trigger Render deploy hook + env: + REGISTRY: ${{ env.REGISTRY }} + DEPLOY_HOOK_URL: ${{ secrets.RENDER_DEPLOY_HOOK_SERVER }} + IMAGE_TAG: ${{ github.sha }} + run: | + if [ -z "$DEPLOY_HOOK_URL" ]; then + echo "RENDER_DEPLOY_HOOK_SERVER not set; skipping deploy (see docs/deployment.md)." + exit 0 + fi + curl -fsS -X POST \ + "${DEPLOY_HOOK_URL}&imgURL=${REGISTRY}/${GITHUB_REPOSITORY,,}/server:${IMAGE_TAG}" + + deploy-nlp-service: + name: Deploy nlp-service to Render + needs: build-nlp-service + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Trigger Render deploy hook + env: + REGISTRY: ${{ env.REGISTRY }} + DEPLOY_HOOK_URL: ${{ secrets.RENDER_DEPLOY_HOOK_NLP_SERVICE }} + IMAGE_TAG: ${{ github.sha }} + run: | + if [ -z "$DEPLOY_HOOK_URL" ]; then + echo "RENDER_DEPLOY_HOOK_NLP_SERVICE not set; skipping deploy (see docs/deployment.md)." + exit 0 + fi + curl -fsS -X POST \ + "${DEPLOY_HOOK_URL}&imgURL=${REGISTRY}/${GITHUB_REPOSITORY,,}/nlp-service:${IMAGE_TAG}" diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..d9579ba --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,92 @@ +# Deployment + +## Target: Render, "Deploy an existing image" (Web Services) + +`server` (Express, port 3000) and `nlp-service` (FastAPI, port 8000) are both +stateless HTTP services with no local database — `server.ts` talks to an +external data platform (`MUJARRAD_*` env vars) and both services call the +Gemini API (`GEMINI_API_KEY`). Neither has a persistence requirement that +would push this toward a heavier platform. + +Options considered, and why they were ruled out: + +- **Vercel** — built for serverless/edge functions and static frontends, not + a great fit for two always-on containers with their own process/port. +- **Fly.io** — good for plain Docker apps, but it has no native way to pull + from a third-party private registry like GHCR. The supported workaround is + to `docker pull` the GHCR image in CI and re-push it into Fly's own + registry before `flyctl deploy` — an extra hop that duplicates image + storage for no benefit here. +- **AWS (ECS/Fargate)** — can pull directly from a private registry, but + needs a VPC, cluster, task definitions, and an ALB provisioned first. That + is more standing infrastructure than this repo's current stage justifies. +- **Render** — supports "Deploy an existing image" directly from a private + GHCR image via a stored registry credential, and gives each service a + deploy-hook URL that CI can call with a specific image tag. This is the + smallest amount of new infrastructure that satisfies "pull the published + GHCR image and run it." + +Both services deploy to Render as separate Web Services running the +prebuilt images published by `.github/workflows/docker-publish.yml` +(`ghcr.io/wider-community/resolve-ai/server` and `.../nlp-service`). + +## One-time setup (needs a human with Render account access) + +This repo's CI cannot create Render resources or hold Render/GHCR +credentials, so the following is a manual setup, done once, by whoever owns +(or is granted) the Wider-Community Render account: + +1. **Registry credential** — in the Render workspace, go to + Settings → Container Registry Credentials → add a GHCR credential using a + GitHub Personal Access Token with `read:packages` scope (a token from a + dedicated bot/service account is preferable to a personal one — the + built-in per-workflow `GITHUB_TOKEN` can't be used here since it's + ephemeral and can't be typed into the Render dashboard). + +2. **`resolve-ai-server` Web Service** — "Deploy an existing image": + - Image: `ghcr.io/wider-community/resolve-ai/server:latest`, using the + credential from step 1. + - Port: `3000` + - Env vars: `GEMINI_API_KEY`, `MUJARRAD_API_PUBLIC_KEY`, + `MUJARRAD_API_SECRET_KEY`, `MUJARRAD_SPACE_SLUG` — values come from + whoever owns those credentials; they don't live in this repo or CI. + +3. **`resolve-ai-nlp-service` Web Service** — "Deploy an existing image": + - Image: `ghcr.io/wider-community/resolve-ai/nlp-service:latest`, same + credential. + - Port: `8000` + - Env vars: `GEMINI_API_KEY` + +4. **Deploy hooks** — on each service, Settings → Deploy Hook, copy the URL, + and add it as a GitHub Actions repository secret: + - `RENDER_DEPLOY_HOOK_SERVER` + - `RENDER_DEPLOY_HOOK_NLP_SERVICE` + +Until these secrets exist, the deploy jobs added in this PR no-op (see +below) — merging this PR does not require the Render services to exist +yet, but nothing will actually deploy until someone completes the steps +above. + +## CI/CD wiring + +`.github/workflows/docker-publish.yml` gets a `deploy-server` and a +`deploy-nlp-service` job, each running after its image finishes publishing +on a push to `main`. Each job POSTs to its Render deploy hook with an +`imgURL` query param pinned to the commit SHA that was just built, so the +service that comes up always matches the exact commit that triggered the +build rather than a possibly-stale `:latest`. + +Each deploy job no-ops (exits 0 without deploying, not failing) when its +deploy-hook secret isn't set yet, so this workflow change is safe to merge +ahead of the Render setup above. The empty-secret check runs in the step's +shell rather than a job-level `if:`, because GitHub Actions does not expose +the `secrets` context to job-level `if:` conditions. + +## Follow-ups intentionally out of scope here + +- Render's free tier spins services down after inactivity (slow cold + start on the next request). Fine for now; revisit if this becomes + user-facing with latency requirements. +- No staging environment — both services deploy straight to what Render + calls production on every push to `main`, matching the rest of this + repo's CI/CD (no branch protection yet either — see WID-216).