The starter includes the Express/TypeScript app, a health endpoint test, TypeScript configuration, and the basic npm scripts.
Run these commands in Bash or Zsh (Git Bash on Windows).
git clone https://github.com/bradtraversy/cicd-lab-starter.git cicd-lab
cd cicd-lab
code .From inside the newly cloned cicd-lab folder, remove the starter's Git history and remote connection, then initialize your own repository on main. Your app files are preserved.
rm -rf .git
git init -b mainThe starter already declares Node.js 24 in engines and includes matching Node types. Check your local version before installing dependencies:
node --versionIf the output does not start with v24., install or switch to Node.js 24 before continuing. The engines field does not switch your local Node version.
Install dependencies from the lockfile:
npm cinpm run devVisit http://localhost:3000 and http://localhost:3000/api/health. The health endpoint should return HTTP 200 with this JSON:
{"status":"ok"}The starter includes these scripts in package.json:
"scripts": {
"dev": "tsx watch src/server.ts",
"typecheck": "tsc --noEmit",
"test": "node --import tsx --test test/app.test.ts",
"build": "tsc",
"start": "node dist/server.js"
}The test in test/app.test.ts starts its own server and checks that /api/health returns HTTP 200 and { "status": "ok" }.
Run the checks in a second terminal. The build compiles JavaScript into dist.
npm test
npm run typecheck
npm run buildAdd this entry inside the scripts object in package.json, including the necessary comma between entries:
"verify": "npm run typecheck && npm test && npm run build"Run all checks with one command. If a check fails, && stops the remaining commands.
npm run verifyThe starter's .gitignore excludes dependencies, build output, and environment secrets. Include package-lock.json in your commit.
git status --short
git add .
git commit -m "feat: add starter app with local verification"Create an empty cicd-lab repository on GitHub without a README, license, or gitignore. Use a public repository to use rulesets on GitHub Free.
git remote add origin https://github.com/YOUR_USERNAME/cicd-lab.git
git push -u origin mainConnect your GitHub account to Render and create a Node Web Service from your own repository, branch main, with a blank root directory and the Free instance for this demo.
Build Command:
npm ci && npm run buildStart Command:
npm startHealth Check Path: /api/health
Auto-Deploy: On Commit
Wait for a successful deployment, then confirm the public home page and /api/health response.
git switch -c feature/github-actions-cimkdir -p .github/workflowsCreate .github/workflows/ci.yml; the job runs for pull requests into main and pushes to main.
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
verify:
name: Verify
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify application
run: npm run verifyStop the development server, then reproduce the CI installation and checks locally.
npm ci
npm run verifyReview, commit, and publish this branch.
git add .
git commit -m "ci: add GitHub Actions verification workflow"
git push -u origin feature/github-actions-ciOpen a pull request into main on GitHub.
Wait for Verify to pass, inspect its Actions logs, then merge; merging triggers another run for main.
Merge the pull request using Create a merge commit, click Delete branch on GitHub if it is still available, then run:
git switch main
git pull --ff-only
git branch -d feature/github-actions-ciAfter the GitHub Actions workflow is merged, wait for the Verify run on main to pass. In the Render web service settings, change Auto-Deploy from On Commit to After CI Checks Pass. Future pushes to main will deploy only after their CI checks pass.
In GitHub Settings > Rules > Rulesets, create Protect main with these settings; the workflow reports results and the ruleset enforces them.
Enforcement: Active
Target: Default branch (main)
Require a pull request before merging: Enabled
Required approvals: 0 for this solo demo
Require status checks to pass: Enabled
Required check: Verify (GitHub Actions)
Bypass list: Empty
git switch -c feature/update-homepageAdd to the home page HTML in src/app.ts.
<p>Verified by GitHub Actions. Deployed on Render.</p>Run the local checks.
npm run verifyReview, commit, and publish this branch.
git add .
git commit -m "feat: update homepage deployment message"
git push -u origin feature/update-homepageOpen a pull request into main on GitHub.
Wait for Verify before merging.
Merge the pull request using Create a merge commit, click Delete branch on GitHub if it is still available, then run:
git switch main
git pull --ff-only
git branch -d feature/update-homepageWait for the main check and Render deployment, then confirm the new message on the public page.
git switch -c feature/version-endpointAppend to test/app.test.ts before creating the route.
test('GET /api/version returns the current version', async () => {
const response = await fetch(`${baseUrl}/api/version`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
version: process.env.APP_VERSION ?? 'development',
});
});This deliberately fails with HTTP 404 instead of 200.
npm testReview, commit, and publish this branch.
git add .
git commit -m "test: demonstrate missing version endpoint"
git push -u origin feature/version-endpointOpen a pull request into main on GitHub.
Inspect the failed Verify job and blocked merge; leave this pull request open.
Fix the failure by adding this route before export default app in src/app.ts.
app.get('/api/version', (_request, response) => {
response.json({
version: process.env.APP_VERSION ?? 'development',
});
});Confirm both tests and the build pass.
npm run verifygit add .
git commit -m "feat: add version endpoint"
git pushThe existing pull request updates automatically; wait for its new Verify run to pass.
Merge the pull request using Create a merge commit, click Delete branch on GitHub if it is still available, then run:
git switch main
git pull --ff-only
git branch -d feature/version-endpointAfter deployment, open /api/version; with no APP_VERSION set, expect:
{"version":"development"}A preview deployment is a temporary copy of your app built from a pull request, with its own URL, so you can check the change in a browser before merging. Production keeps serving main, and Render deletes the preview when the pull request is merged or closed.
In the Render Dashboard, open your web service's Previews tab, set Pull Request Previews to Automatic, and save. Each preview is a separate running instance, so check the pricing shown. To preview only selected pull requests, choose Manual and put [render preview] in the pull request title. See Render service previews.
git switch -c feature/preview-demoAdd to the home page HTML in src/app.ts.
<p>Preview deployment</p>Verify the change.
npm run verifyReview, commit, and publish this branch.
git add .
git commit -m "feat: add preview deployment message"
git push -u origin feature/preview-demoOpen a pull request into main on GitHub and leave it unmerged. With Automatic enabled, use a normal title:
Add preview deployment message
If you selected Manual instead, use this title to request a preview:
[render preview] Add preview deployment message
Opening the pull request triggers the preview; you do not need to merge it first. Wait for Verify and the preview deployment. Open the preview from the pull request's View deployment link or the service's Previews tab in Render.
Compare the two URLs while the pull request is still open: the preview should display the new message, and production should not. Only merge after reviewing the preview.
Merge the pull request using Create a merge commit, click Delete branch on GitHub if it is still available, then run:
git switch main
git pull --ff-only
git branch -d feature/preview-demoConfirm the production message after deployment and that Render removes the temporary preview.