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
44 changes: 44 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
92 changes: 92 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -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).
Loading