Repository navigation
Deployment
Two GitHub Actions workflows:
-
Node.js CI(.github/workflows/node.js.yml) — on push/PR tomainandstaging:npm ci,npm run build --if-present,npm test, on Node 20.x and 22.x. Bothbuild (20.x)andbuild (22.x)are required status checks on both branches — a PR cannot be merged (by anyone, including the repo owner) until both are green on that exact commit. -
Build & Deploy to Render(.github/workflows/deploy.yml) — triggered only on push tomain:npm install,npm run build, thencurls Render's deploy hook URL (RENDER_SRV/RENDER_KEYsecrets).
Important: the deploy workflow's "success" only means the deploy hook was triggered (HTTP 2xx from Render's API) — it does not wait for or reflect the result of Render's own build. Render pulls the repo and does its own independent npm install/build after being triggered; that can fail even when the GitHub Actions run shows green. Always verify a deploy by hitting the live URL directly, not by trusting the Actions status alone.
Live URL: https://nodejs-resume-api-ts.onrender.com
This replaced an earlier develop → main model on 2026-08-30. See CONTRIBUTING.md at the repo root for the full rationale; summary:
- Every feature/fix/hotfix branch is cut from
staging, never frommain. - PRs merge into
staging— never straight intomainfrom a feature branch. -
staging→mainonly happens through an explicit release step (merge commit, not squash, sogit bisect/git blamestill resolve to the real originating commit). - Both
mainandstaginghave real GitHub branch protection: no direct push (enforce_admins: true, so not even the owner can bypass it), and both are gated by the samebuild (20.x)/build (22.x)required checks described above. -
required_approving_review_countis deliberately0on both — this is a solo-maintainer repo (human + Claude Code both merge PRs), and GitHub refuses to let an account approve its own PR, so requiring ≥1 review would lock the only maintainer out entirely.0still forces every change through a reviewable, diffable PR — it just doesn't add a review gate that can't be satisfied. - An issue only auto-closes (
Closes #N) when the PR merges into the default branch, which ismain— a PR merging intostagingnever auto-closes its issue, even if the body saysCloses #N; that's expected, not a bug, and fires for real once the same change reachesmainvia the release PR.
If using Claude Code: /ship handles feature-branch → staging (commit, push, optionally open+merge the PR); /release handles staging → main (build+test gate, merge commit, version bump, tag, deploy trigger, issue-closing).
No local Node/Mongo install needed — everything runs in containers, and this is the easiest way to reproduce the exact production topology locally.
Dev (hot reload, insecure built-in dev secrets, no .env required):
docker compose up --build→ http://localhost:3001/health, includes a MongoDB 7 container with a persistent volume.
Prod (compiled image, real secrets required):
cp .env.example .env # fill in real TOKEN_SECRET/TOKEN_REFRESH/SESSION_SECRET/CORS_ORIGIN
docker compose -f docker-compose.prod.yml up -d --build→ http://localhost:3008/health
Chromium is installed via apt in the image (not Puppeteer's own downloader) with PUPPETEER_EXECUTABLE_PATH set — this specifically works around the hardcoded-Chrome-path fragility noted in Security. Redis is intentionally not containerized in either stack — the app already falls back to in-memory when REDIS_URL is unset.
Two real bugs were found and fixed while building/live-testing the Docker setup (both now fixed on main): production always ignoring LOCAL_PORT and hardcoding port 3008 regardless of the env var, and three file-write paths (createPDF.ts, uploadCV.middleware.ts, uploadImages.middleware.ts) being hardcoded as src/public/... instead of resolving relative to __dirname — which 500'd with ENOENT in a minimal production image that only ships dist/, not src/.
The app depends on the full puppeteer package (not puppeteer-core), which downloads a Chrome binary during npm install. Render persists a build cache (/opt/render/.cache/puppeteer/...) across deploys to speed this up. If a previous build was interrupted mid-download, the cache can end up in a state where the destination folder exists but the actual executable inside is missing — Puppeteer's installer sees the folder, assumes it's already installed, and fails instead of re-downloading:
Error: The browser folder (.../chrome-linux64) exists but the executable (.../chrome) is missing
Fix: on Render's dashboard, use Manual Deploy → "Clear build cache & deploy". This has happened at least once and self-resolved on a subsequent deploy without intervention, but don't assume that will always happen — clear the cache if a deploy fails with this error.
Render's environment variables are configured in its dashboard, separately from this repo's .env.example/.env.development/.env.production files. See Getting Started for the full variable list, including CORS_ORIGIN — Render needs the same set. The real production value of CORS_ORIGIN is a dashboard setting, not tracked in this repo; verify it directly on Render rather than assuming it matches any local .env.
.env's MONGO_URI/MONGOBD_USER happens to point at the same database Render uses, your "local" testing is not isolated — double-check which database you're actually pointed at before running destructive tests or scripts (e.g. migrate:localize-text) against it.