Skip to content

Commit 4b455da

Browse files
Add Render deploy step for server and nlp-service images (#2)
* Add Render deploy step for server and nlp-service images Picks Render "deploy an existing image" as the hosting target for both GHCR-published images (server on :3000, nlp-service on :8000) since it natively supports pulling from a private registry and exposes a deploy-hook API that CI can trigger with a specific image tag. Fly.io and AWS were ruled out as heavier fits for this project's current stage (see docs/deployment.md for the full comparison). The docker-publish workflow gains deploy-server/deploy-nlp-service jobs that POST the commit-SHA-tagged image to each service's Render deploy hook. Both jobs no-op until RENDER_DEPLOY_HOOK_SERVER / RENDER_DEPLOY_HOOK_NLP_SERVICE secrets exist, so this is safe to merge ahead of the one-time Render setup, which is documented in docs/deployment.md along with the required env vars and secrets. * Fix invalid secrets context in deploy job if-conditions GitHub Actions does not expose the `secrets` context to job-level `if:` conditions, so `if: ... && secrets.RENDER_DEPLOY_HOOK_* != ''` made the whole docker-publish workflow invalid (actionlint: "context 'secrets' is not allowed here"). That would have broken all image publishing on main, not just skipped the deploy jobs. Gate the deploy jobs on `github.ref == 'refs/heads/main'` only, and move the empty-secret check into the step's shell (secrets are valid in step `env:`), exiting 0 as a no-op when the deploy hook secret isn't set. This preserves the "safe to merge ahead of Render setup" behavior with a mechanism GitHub actually supports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a43d124 commit 4b455da

2 files changed

Lines changed: 136 additions & 0 deletions

File tree

.github/workflows/docker-publish.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,47 @@ jobs:
6363
tags: |
6464
${{ steps.image.outputs.name }}:latest
6565
${{ steps.image.outputs.name }}:${{ github.sha }}
66+
67+
# Deploy jobs trigger a Render deploy hook pinned to the image tag that was
68+
# just published (see docs/deployment.md for the one-time Render setup).
69+
# The hook URL is a secret; when it isn't configured the step no-ops (exits
70+
# 0 without deploying), so this workflow is safe to merge ahead of that
71+
# setup. Note: the `secrets` context is NOT available in a job-level `if:`,
72+
# so the presence check is done in the step's shell, not the `if:`.
73+
deploy-server:
74+
name: Deploy server to Render
75+
needs: build-server
76+
runs-on: ubuntu-latest
77+
if: github.ref == 'refs/heads/main'
78+
steps:
79+
- name: Trigger Render deploy hook
80+
env:
81+
REGISTRY: ${{ env.REGISTRY }}
82+
DEPLOY_HOOK_URL: ${{ secrets.RENDER_DEPLOY_HOOK_SERVER }}
83+
IMAGE_TAG: ${{ github.sha }}
84+
run: |
85+
if [ -z "$DEPLOY_HOOK_URL" ]; then
86+
echo "RENDER_DEPLOY_HOOK_SERVER not set; skipping deploy (see docs/deployment.md)."
87+
exit 0
88+
fi
89+
curl -fsS -X POST \
90+
"${DEPLOY_HOOK_URL}&imgURL=${REGISTRY}/${GITHUB_REPOSITORY,,}/server:${IMAGE_TAG}"
91+
92+
deploy-nlp-service:
93+
name: Deploy nlp-service to Render
94+
needs: build-nlp-service
95+
runs-on: ubuntu-latest
96+
if: github.ref == 'refs/heads/main'
97+
steps:
98+
- name: Trigger Render deploy hook
99+
env:
100+
REGISTRY: ${{ env.REGISTRY }}
101+
DEPLOY_HOOK_URL: ${{ secrets.RENDER_DEPLOY_HOOK_NLP_SERVICE }}
102+
IMAGE_TAG: ${{ github.sha }}
103+
run: |
104+
if [ -z "$DEPLOY_HOOK_URL" ]; then
105+
echo "RENDER_DEPLOY_HOOK_NLP_SERVICE not set; skipping deploy (see docs/deployment.md)."
106+
exit 0
107+
fi
108+
curl -fsS -X POST \
109+
"${DEPLOY_HOOK_URL}&imgURL=${REGISTRY}/${GITHUB_REPOSITORY,,}/nlp-service:${IMAGE_TAG}"

docs/deployment.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Deployment
2+
3+
## Target: Render, "Deploy an existing image" (Web Services)
4+
5+
`server` (Express, port 3000) and `nlp-service` (FastAPI, port 8000) are both
6+
stateless HTTP services with no local database — `server.ts` talks to an
7+
external data platform (`MUJARRAD_*` env vars) and both services call the
8+
Gemini API (`GEMINI_API_KEY`). Neither has a persistence requirement that
9+
would push this toward a heavier platform.
10+
11+
Options considered, and why they were ruled out:
12+
13+
- **Vercel** — built for serverless/edge functions and static frontends, not
14+
a great fit for two always-on containers with their own process/port.
15+
- **Fly.io** — good for plain Docker apps, but it has no native way to pull
16+
from a third-party private registry like GHCR. The supported workaround is
17+
to `docker pull` the GHCR image in CI and re-push it into Fly's own
18+
registry before `flyctl deploy` — an extra hop that duplicates image
19+
storage for no benefit here.
20+
- **AWS (ECS/Fargate)** — can pull directly from a private registry, but
21+
needs a VPC, cluster, task definitions, and an ALB provisioned first. That
22+
is more standing infrastructure than this repo's current stage justifies.
23+
- **Render** — supports "Deploy an existing image" directly from a private
24+
GHCR image via a stored registry credential, and gives each service a
25+
deploy-hook URL that CI can call with a specific image tag. This is the
26+
smallest amount of new infrastructure that satisfies "pull the published
27+
GHCR image and run it."
28+
29+
Both services deploy to Render as separate Web Services running the
30+
prebuilt images published by `.github/workflows/docker-publish.yml`
31+
(`ghcr.io/wider-community/resolve-ai/server` and `.../nlp-service`).
32+
33+
## One-time setup (needs a human with Render account access)
34+
35+
This repo's CI cannot create Render resources or hold Render/GHCR
36+
credentials, so the following is a manual setup, done once, by whoever owns
37+
(or is granted) the Wider-Community Render account:
38+
39+
1. **Registry credential** — in the Render workspace, go to
40+
Settings → Container Registry Credentials → add a GHCR credential using a
41+
GitHub Personal Access Token with `read:packages` scope (a token from a
42+
dedicated bot/service account is preferable to a personal one — the
43+
built-in per-workflow `GITHUB_TOKEN` can't be used here since it's
44+
ephemeral and can't be typed into the Render dashboard).
45+
46+
2. **`resolve-ai-server` Web Service** — "Deploy an existing image":
47+
- Image: `ghcr.io/wider-community/resolve-ai/server:latest`, using the
48+
credential from step 1.
49+
- Port: `3000`
50+
- Env vars: `GEMINI_API_KEY`, `MUJARRAD_API_PUBLIC_KEY`,
51+
`MUJARRAD_API_SECRET_KEY`, `MUJARRAD_SPACE_SLUG` — values come from
52+
whoever owns those credentials; they don't live in this repo or CI.
53+
54+
3. **`resolve-ai-nlp-service` Web Service** — "Deploy an existing image":
55+
- Image: `ghcr.io/wider-community/resolve-ai/nlp-service:latest`, same
56+
credential.
57+
- Port: `8000`
58+
- Env vars: `GEMINI_API_KEY`
59+
60+
4. **Deploy hooks** — on each service, Settings → Deploy Hook, copy the URL,
61+
and add it as a GitHub Actions repository secret:
62+
- `RENDER_DEPLOY_HOOK_SERVER`
63+
- `RENDER_DEPLOY_HOOK_NLP_SERVICE`
64+
65+
Until these secrets exist, the deploy jobs added in this PR no-op (see
66+
below) — merging this PR does not require the Render services to exist
67+
yet, but nothing will actually deploy until someone completes the steps
68+
above.
69+
70+
## CI/CD wiring
71+
72+
`.github/workflows/docker-publish.yml` gets a `deploy-server` and a
73+
`deploy-nlp-service` job, each running after its image finishes publishing
74+
on a push to `main`. Each job POSTs to its Render deploy hook with an
75+
`imgURL` query param pinned to the commit SHA that was just built, so the
76+
service that comes up always matches the exact commit that triggered the
77+
build rather than a possibly-stale `:latest`.
78+
79+
Each deploy job no-ops (exits 0 without deploying, not failing) when its
80+
deploy-hook secret isn't set yet, so this workflow change is safe to merge
81+
ahead of the Render setup above. The empty-secret check runs in the step's
82+
shell rather than a job-level `if:`, because GitHub Actions does not expose
83+
the `secrets` context to job-level `if:` conditions.
84+
85+
## Follow-ups intentionally out of scope here
86+
87+
- Render's free tier spins services down after inactivity (slow cold
88+
start on the next request). Fine for now; revisit if this becomes
89+
user-facing with latency requirements.
90+
- No staging environment — both services deploy straight to what Render
91+
calls production on every push to `main`, matching the rest of this
92+
repo's CI/CD (no branch protection yet either — see WID-216).

0 commit comments

Comments
 (0)