{"id":176,"date":"2026-10-06T13:45:10","date_gmt":"2026-10-06T13:45:10","guid":{"rendered":"https:\/\/devdojo.co.in\/?p=176"},"modified":"2026-10-06T13:45:11","modified_gmt":"2026-10-06T13:45:11","slug":"github-actions-ci-cd-nodejs","status":"publish","type":"post","link":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/","title":{"rendered":"GitHub Actions CI\/CD for Node.js: A Complete Beginner&#8217;s Guide"},"content":{"rendered":"\n<div class=\"dd-post\">\n<style>\n.single article:has(.dd-hero) .entry-media{display:none}\n.dd-post{--dd-navy:#0b1020;--dd-blue:#3178c6;--dd-violet:#7c5cff;--dd-ink:#1d2333;--dd-muted:#5b6478;--dd-line:#e3e7f0;font-size:17px;line-height:1.75;color:var(--dd-ink)}\n.dd-post h2{margin:2.2em 0 .6em;font-size:1.6em;line-height:1.3;scroll-margin-top:90px}\n.dd-post h3{margin:1.6em 0 .5em;font-size:1.25em;scroll-margin-top:90px}\n.dd-post a{color:var(--dd-blue)}\n.dd-post .dd-hero{margin:0 0 28px;border-radius:16px;overflow:hidden;box-shadow:0 12px 40px rgba(11,16,32,.35);background:#0b1020}\n.dd-post .dd-hero svg{display:block;width:100%;height:auto}\n.dd-post .dd-hero .node rect{transition:stroke .25s,fill .25s}\n.dd-post .dd-hero .node:hover rect{stroke:#7c5cff;fill:#1e2656}\n.dd-post .dd-hero .node:hover text{fill:#fff}\n.dd-post pre{background:#0f1430;color:#e6e9f5;padding:18px 20px;border-radius:12px;overflow-x:auto;margin:1.2em 0;border:1px solid #232a52;font-size:14.5px !important;line-height:1.6}\n.dd-post pre code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:1em !important;background:none;color:inherit;padding:0;white-space:pre}\n.dd-post :not(pre)>code{background:#eef1fb;color:#3a2fa0;padding:2px 6px;border-radius:5px;font-size:.9em}\n.dd-post .dd-file{display:inline-block;margin:1em 0 -0.9em;background:#232a52;color:#c8cff5;font:600 12.5px\/1 ui-monospace,Menlo,monospace;padding:7px 12px;border-radius:8px 8px 0 0}\n.dd-post .dd-summary{background:linear-gradient(135deg,#eef3ff,#f3efff);border:1px solid #d9def7;border-left:5px solid var(--dd-violet);border-radius:12px;padding:18px 22px;margin:0 0 26px}\n.dd-post .dd-summary strong.dd-label{display:block;font-size:.85em;letter-spacing:.06em;text-transform:uppercase;color:var(--dd-violet);margin-bottom:6px}\n.dd-post .dd-summary ul{margin:0;padding-left:20px}\n.dd-post .dd-toc{background:#f7f8fc;border:1px solid var(--dd-line);border-radius:12px;padding:16px 22px;margin:0 0 28px}\n.dd-post .dd-toc strong{display:block;margin-bottom:6px}\n.dd-post .dd-toc ol{margin:0;padding-left:22px;columns:2;column-gap:32px}\n.dd-post .dd-toc li{break-inside:avoid;margin:2px 0}\n@media (max-width:640px){.dd-post .dd-toc ol{columns:1}}\n.dd-post .dd-tip,.dd-post .dd-note,.dd-post .dd-warn{border-radius:10px;padding:14px 18px;margin:1.2em 0}\n.dd-post .dd-tip{background:#ecfbf3;border-left:5px solid #1fa463}\n.dd-post .dd-note{background:#eef4fe;border-left:5px solid var(--dd-blue)}\n.dd-post .dd-warn{background:#fff6e8;border-left:5px solid #e8930c}\n.dd-post .dd-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px;margin:1.2em 0}\n.dd-post .dd-card{border:1px solid var(--dd-line);border-radius:12px;padding:14px 16px;background:#fff;box-shadow:0 2px 8px rgba(20,30,70,.05)}\n.dd-post .dd-card b{display:block;color:var(--dd-violet);margin-bottom:4px}\n.dd-post table{width:100%;border-collapse:collapse;margin:1.2em 0;font-size:.93em;display:block;overflow-x:auto}\n.dd-post th,.dd-post td{border:1px solid var(--dd-line);padding:10px 12px;text-align:left;vertical-align:top}\n.dd-post th{background:#151b3b;color:#fff}\n.dd-post tr:nth-child(even) td{background:#f8f9fd}\n.dd-post figure{margin:1.6em 0;text-align:center}\n.dd-post figure svg{width:100%;height:auto;border-radius:12px}\n.dd-post figcaption{font-size:.88em;color:var(--dd-muted);margin-top:8px}\n.dd-post details{border:1px solid var(--dd-line);border-radius:10px;padding:12px 16px;margin:10px 0;background:#fff}\n.dd-post details summary{cursor:pointer;font-weight:600}\n.dd-post details[open] summary{margin-bottom:8px;color:var(--dd-violet)}\n.dd-post .dd-tabs{margin:1.4em 0;border:1px solid var(--dd-line);border-radius:12px;overflow:hidden;background:#fff}\n.dd-post .dd-tabs input{position:absolute;opacity:0;pointer-events:none}\n.dd-post .dd-tabs .dd-tablist{display:flex;flex-wrap:wrap;background:#151b3b}\n.dd-post .dd-tabs label{padding:11px 16px;color:#b8c0e8;cursor:pointer;font-weight:600;font-size:.92em;border-bottom:3px solid transparent}\n.dd-post .dd-tabs label:hover{color:#fff}\n.dd-post .dd-tabs .dd-panel{display:none;padding:6px 18px 14px}\n.dd-post #dd-t1:checked~.dd-tablist label[for=dd-t1],.dd-post #dd-t2:checked~.dd-tablist label[for=dd-t2],.dd-post #dd-t3:checked~.dd-tablist label[for=dd-t3],.dd-post #dd-t4:checked~.dd-tablist label[for=dd-t4]{color:#fff;border-bottom-color:#7c5cff;background:#1e2656}\n.dd-post #dd-t1:checked~.dd-p1,.dd-post #dd-t2:checked~.dd-p2,.dd-post #dd-t3:checked~.dd-p3,.dd-post #dd-t4:checked~.dd-p4{display:block}\n.dd-post #dd-t1:focus-visible~.dd-tablist label[for=dd-t1],.dd-post #dd-t2:focus-visible~.dd-tablist label[for=dd-t2],.dd-post #dd-t3:focus-visible~.dd-tablist label[for=dd-t3],.dd-post #dd-t4:focus-visible~.dd-tablist label[for=dd-t4]{outline:2px solid #7c5cff;outline-offset:-2px}\n.dd-post .dd-cta{margin:2em 0 1em;padding:24px 26px;border-radius:14px;background:linear-gradient(135deg,#0b1020,#151b3b);color:#e6e9f5;text-align:center}\n.dd-post .dd-cta h3{color:#fff;margin:0 0 6px}\n.dd-post .dd-cta p{margin:0;color:#c3c9ea}\n.dd-post .dd-cta a{color:#a99bff;font-weight:600}\n<\/style>\n<div class=\"dd-hero\">\n<svg viewBox=\"0 0 1200 520\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"ddh-t ddh-d\">\n<title id=\"ddh-t\">GitHub Actions CI\/CD for Node.js<\/title>\n<desc id=\"ddh-d\">Animated banner showing a pipeline: push, install, test, build and deploy, with data dots moving between the steps.<\/desc>\n<defs>\n<linearGradient id=\"ddh-bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#0b1020\"\/><stop offset=\"1\" stop-color=\"#151b3b\"\/><\/linearGradient>\n<linearGradient id=\"ddh-ac\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#3178c6\"\/><stop offset=\"1\" stop-color=\"#7c5cff\"\/><\/linearGradient>\n<radialGradient id=\"ddh-glow\" cx=\".5\" cy=\".5\" r=\".5\"><stop offset=\"0\" stop-color=\"#7c5cff\" stop-opacity=\".35\"\/><stop offset=\"1\" stop-color=\"#7c5cff\" stop-opacity=\"0\"\/><\/radialGradient>\n<pattern id=\"ddh-grid\" width=\"40\" height=\"40\" patternUnits=\"userSpaceOnUse\"><path d=\"M40 0H0V40\" fill=\"none\" stroke=\"#ffffff\" stroke-opacity=\".04\"\/><\/pattern>\n<\/defs>\n<rect x=\"0\" y=\"-60\" width=\"1200\" height=\"640\" fill=\"url(#ddh-bg)\"\/>\n<rect x=\"0\" y=\"-60\" width=\"1200\" height=\"640\" fill=\"url(#ddh-grid)\"\/>\n<circle cx=\"980\" cy=\"90\" r=\"220\" fill=\"url(#ddh-glow)\"\/>\n<circle cx=\"160\" cy=\"470\" r=\"200\" fill=\"url(#ddh-glow)\"\/>\n<text x=\"70\" y=\"92\" fill=\"#9aa6d8\" font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"20\" font-weight=\"600\" letter-spacing=\"3\">DEVDOJO \u00b7 DEVOPS<\/text>\n<text x=\"70\" y=\"160\" fill=\"#ffffff\" font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"54\" font-weight=\"800\">GitHub Actions CI\/CD for Node.js<\/text>\n<text x=\"70\" y=\"210\" fill=\"#c3c9ea\" font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"25\">Test, build and ship your app automatically on every push<\/text>\n<rect x=\"70\" y=\"232\" width=\"190\" height=\"5\" rx=\"2.5\" fill=\"url(#ddh-ac)\"\/>\n<path id=\"ddh-path\" d=\"M150 380 H1050\" stroke=\"url(#ddh-ac)\" stroke-width=\"4\" stroke-dasharray=\"10 10\" fill=\"none\" opacity=\".7\"\/>\n<g font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"20\" font-weight=\"700\" text-anchor=\"middle\">\n<g class=\"node\"><rect x=\"80\" y=\"340\" width=\"140\" height=\"80\" rx=\"16\" fill=\"#151b3b\" stroke=\"#3178c6\" stroke-width=\"2.5\"\/><text x=\"150\" y=\"374\" fill=\"#c3c9ea\" font-size=\"15\" font-weight=\"500\">git<\/text><text x=\"150\" y=\"400\" fill=\"#e6e9f5\">Push<\/text><\/g>\n<g class=\"node\"><rect x=\"305\" y=\"340\" width=\"140\" height=\"80\" rx=\"16\" fill=\"#151b3b\" stroke=\"#3178c6\" stroke-width=\"2.5\"\/><text x=\"375\" y=\"374\" fill=\"#c3c9ea\" font-size=\"15\" font-weight=\"500\">npm ci<\/text><text x=\"375\" y=\"400\" fill=\"#e6e9f5\">Install<\/text><\/g>\n<g class=\"node\"><rect x=\"530\" y=\"340\" width=\"140\" height=\"80\" rx=\"16\" fill=\"#151b3b\" stroke=\"#7c5cff\" stroke-width=\"2.5\"\/><text x=\"600\" y=\"374\" fill=\"#c3c9ea\" font-size=\"15\" font-weight=\"500\">node &#8211;test<\/text><text x=\"600\" y=\"400\" fill=\"#e6e9f5\">Test<\/text><\/g>\n<g class=\"node\"><rect x=\"755\" y=\"340\" width=\"140\" height=\"80\" rx=\"16\" fill=\"#151b3b\" stroke=\"#7c5cff\" stroke-width=\"2.5\"\/><text x=\"825\" y=\"374\" fill=\"#c3c9ea\" font-size=\"15\" font-weight=\"500\">docker<\/text><text x=\"825\" y=\"400\" fill=\"#e6e9f5\">Build<\/text><\/g>\n<g class=\"node\"><rect x=\"980\" y=\"340\" width=\"140\" height=\"80\" rx=\"16\" fill=\"#151b3b\" stroke=\"#1fa463\" stroke-width=\"2.5\"\/><text x=\"1050\" y=\"374\" fill=\"#c3c9ea\" font-size=\"15\" font-weight=\"500\">production<\/text><text x=\"1050\" y=\"400\" fill=\"#e6e9f5\">Deploy<\/text><\/g>\n<\/g>\n<circle r=\"7\" fill=\"#7c5cff\"><animateMotion dur=\"4s\" repeatCount=\"indefinite\"><mpath href=\"#ddh-path\"\/><\/animateMotion><\/circle>\n<circle r=\"6\" fill=\"#3178c6\"><animateMotion dur=\"4s\" begin=\"1.3s\" repeatCount=\"indefinite\"><mpath href=\"#ddh-path\"\/><\/animateMotion><\/circle>\n<circle r=\"5\" fill=\"#1fa463\"><animateMotion dur=\"4s\" begin=\"2.6s\" repeatCount=\"indefinite\"><mpath href=\"#ddh-path\"\/><\/animateMotion><\/circle>\n<g font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"17\" fill=\"#8f9bd0\"><text x=\"70\" y=\"478\">\u2713 checkout@v7<\/text><text x=\"250\" y=\"478\">\u2713 setup-node@v7<\/text><text x=\"450\" y=\"478\">\u2713 Node 22 + 24 matrix<\/text><text x=\"680\" y=\"478\">\u2713 Docker image to GHCR<\/text><\/g>\n<\/svg>\n<\/div>\n\n<div class=\"dd-summary\">\n<strong class=\"dd-label\">Quick Summary<\/strong>\n<ul>\n<li><strong>GitHub Actions CI\/CD<\/strong> lets GitHub run your tests, build your app and deploy it automatically every time you push code \u2014 for free on public repos.<\/li>\n<li>A workflow is a YAML file in <code>.github\/workflows\/<\/code>. It has <em>triggers<\/em> (when to run), <em>jobs<\/em> (groups of work) and <em>steps<\/em> (single commands).<\/li>\n<li>For Node.js, the core recipe is: <code>actions\/checkout@v7<\/code> \u2192 <code>actions\/setup-node@v7<\/code> \u2192 <code>npm ci<\/code> \u2192 <code>npm test<\/code>.<\/li>\n<li>Add a version matrix (Node 22 and 24), caching, safe permissions, a Docker image push to GitHub Container Registry and a protected production deploy.<\/li>\n<\/ul>\n<\/div>\n\n<nav class=\"dd-toc\" aria-label=\"Table of contents\">\n<strong>Table of Contents<\/strong>\n<ol>\n<li><a href=\"#what-is-ci-cd\">What is CI\/CD?<\/a><\/li>\n<li><a href=\"#how-github-actions-works\">How GitHub Actions works<\/a><\/li>\n<li><a href=\"#sample-node-app\">The sample Node.js app<\/a><\/li>\n<li><a href=\"#first-workflow\">Your first CI workflow<\/a><\/li>\n<li><a href=\"#triggers\">Choosing triggers<\/a><\/li>\n<li><a href=\"#matrix-cache\">Matrix, caching and speed<\/a><\/li>\n<li><a href=\"#test-reports\">Saving test reports<\/a><\/li>\n<li><a href=\"#docker-ghcr\">Build and push a Docker image<\/a><\/li>\n<li><a href=\"#deploy-secrets\">Deploy with secrets and environments<\/a><\/li>\n<li><a href=\"#errors\">Common errors and fixes<\/a><\/li>\n<li><a href=\"#best-practices\">Best practices<\/a><\/li>\n<li><a href=\"#faq\">FAQ<\/a><\/li>\n<\/ol>\n<\/nav>\n\n<p>You fix a bug, push it, and an hour later a teammate tells you the build is broken. Sounds familiar? <strong>GitHub Actions CI\/CD<\/strong> solves this by running your tests and deployment steps automatically on GitHub&#8217;s servers every time code changes. In this beginner-friendly guide, you will build a complete pipeline for a Node.js app \u2014 from a 10-line &#8220;hello CI&#8221; workflow to a production setup with a version matrix, test reports, a Docker image and a protected deploy. Every example uses the current major versions of the official actions, so you can copy them straight into your project.<\/p>\n<\/div>\n\n\n<div class=\"dd-post\">\n<h2 id=\"what-is-ci-cd\">What is CI\/CD? (in plain words)<\/h2>\n<p><strong>CI<\/strong> stands for <em>Continuous Integration<\/em>. Every time someone pushes code or opens a pull request, a robot installs the project and runs the tests. If something breaks, you know within minutes \u2014 not after the code reaches users.<\/p>\n<p><strong>CD<\/strong> stands for <em>Continuous Delivery<\/em> (or <em>Continuous Deployment<\/em>). Once the tests pass, the same robot builds your app and ships it \u2014 to a server, a container registry or a hosting platform.<\/p>\n<div class=\"dd-cards\">\n<div class=\"dd-card\"><b>Without CI\/CD<\/b>&#8220;Works on my machine.&#8221; Manual testing, manual uploads, forgotten steps and late-night surprises.<\/div>\n<div class=\"dd-card\"><b>With CI\/CD<\/b>Every change is tested the same way, on a clean machine, and deployment is one reliable, repeatable process.<\/div>\n<div class=\"dd-card\"><b>Why GitHub Actions?<\/b>It lives right next to your code, needs no extra server, and is free for public repositories (private repos get free monthly minutes).<\/div>\n<\/div>\n\n<h2 id=\"how-github-actions-works\">How GitHub Actions works<\/h2>\n<p>GitHub Actions has only a few building blocks. Once you understand them, every workflow file becomes easy to read.<\/p>\n<table>\n<thead><tr><th>Building block<\/th><th>What it means<\/th><th>Example<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><strong>Workflow<\/strong><\/td><td>One YAML file in <code>.github\/workflows\/<\/code>. A repo can have many.<\/td><td><code>ci.yml<\/code>, <code>release.yml<\/code><\/td><\/tr>\n<tr><td><strong>Event (trigger)<\/strong><\/td><td>What starts the workflow.<\/td><td><code>push<\/code>, <code>pull_request<\/code>, <code>schedule<\/code><\/td><\/tr>\n<tr><td><strong>Job<\/strong><\/td><td>A group of steps that runs on one fresh virtual machine. Jobs run in parallel unless you link them with <code>needs<\/code>.<\/td><td><code>test<\/code>, <code>deploy<\/code><\/td><\/tr>\n<tr><td><strong>Runner<\/strong><\/td><td>The machine that runs a job. GitHub provides Linux, Windows and macOS runners.<\/td><td><code>ubuntu-latest<\/code><\/td><\/tr>\n<tr><td><strong>Step<\/strong><\/td><td>A single shell command (<code>run<\/code>) or a reusable action (<code>uses<\/code>).<\/td><td><code>npm test<\/code><\/td><\/tr>\n<tr><td><strong>Action<\/strong><\/td><td>A ready-made, shareable step published by GitHub or the community.<\/td><td><code>actions\/checkout@v7<\/code><\/td><\/tr>\n<\/tbody>\n<\/table>\n\n<figure>\n<svg viewBox=\"0 0 900 380\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"dd-f1t\">\n<title id=\"dd-f1t\">Diagram: an event triggers a workflow, which contains jobs, which contain steps<\/title>\n<rect width=\"900\" height=\"380\" rx=\"14\" fill=\"#0f1430\"\/>\n<g font-family=\"Segoe UI,Roboto,Arial,sans-serif\">\n<rect x=\"30\" y=\"150\" width=\"150\" height=\"80\" rx=\"12\" fill=\"#151b3b\" stroke=\"#3178c6\" stroke-width=\"2\"\/>\n<text x=\"105\" y=\"185\" fill=\"#fff\" font-size=\"18\" font-weight=\"700\" text-anchor=\"middle\">Event<\/text>\n<text x=\"105\" y=\"210\" fill=\"#9aa6d8\" font-size=\"14\" text-anchor=\"middle\">push \/ pull_request<\/text>\n<path d=\"M180 190 H225\" stroke=\"#7c5cff\" stroke-width=\"3\" marker-end=\"url(#dd-ar)\"\/>\n<rect x=\"235\" y=\"30\" width=\"635\" height=\"320\" rx=\"14\" fill=\"none\" stroke=\"#7c5cff\" stroke-width=\"2\" stroke-dasharray=\"8 6\"\/>\n<text x=\"255\" y=\"60\" fill=\"#c3c9ea\" font-size=\"16\" font-weight=\"700\">Workflow \u00b7 .github\/workflows\/ci.yml<\/text>\n<rect x=\"260\" y=\"80\" width=\"280\" height=\"250\" rx=\"12\" fill=\"#151b3b\" stroke=\"#3178c6\" stroke-width=\"2\"\/>\n<text x=\"280\" y=\"108\" fill=\"#fff\" font-size=\"16\" font-weight=\"700\">Job: test (ubuntu-latest)<\/text>\n<g font-size=\"14\" fill=\"#e6e9f5\">\n<rect x=\"280\" y=\"125\" width=\"240\" height=\"34\" rx=\"8\" fill=\"#232a52\"\/><text x=\"295\" y=\"147\">1. checkout code<\/text>\n<rect x=\"280\" y=\"167\" width=\"240\" height=\"34\" rx=\"8\" fill=\"#232a52\"\/><text x=\"295\" y=\"189\">2. setup Node.js<\/text>\n<rect x=\"280\" y=\"209\" width=\"240\" height=\"34\" rx=\"8\" fill=\"#232a52\"\/><text x=\"295\" y=\"231\">3. npm ci<\/text>\n<rect x=\"280\" y=\"251\" width=\"240\" height=\"34\" rx=\"8\" fill=\"#232a52\"\/><text x=\"295\" y=\"273\">4. npm test<\/text>\n<\/g>\n<path d=\"M540 205 H600\" stroke=\"#7c5cff\" stroke-width=\"3\" marker-end=\"url(#dd-ar)\"\/>\n<text x=\"570\" y=\"195\" fill=\"#9aa6d8\" font-size=\"13\" text-anchor=\"middle\">needs<\/text>\n<rect x=\"610\" y=\"80\" width=\"240\" height=\"250\" rx=\"12\" fill=\"#151b3b\" stroke=\"#1fa463\" stroke-width=\"2\"\/>\n<text x=\"630\" y=\"108\" fill=\"#fff\" font-size=\"16\" font-weight=\"700\">Job: deploy<\/text>\n<g font-size=\"14\" fill=\"#e6e9f5\">\n<rect x=\"630\" y=\"125\" width=\"200\" height=\"34\" rx=\"8\" fill=\"#232a52\"\/><text x=\"645\" y=\"147\">1. build image<\/text>\n<rect x=\"630\" y=\"167\" width=\"200\" height=\"34\" rx=\"8\" fill=\"#232a52\"\/><text x=\"645\" y=\"189\">2. push to registry<\/text>\n<rect x=\"630\" y=\"209\" width=\"200\" height=\"34\" rx=\"8\" fill=\"#232a52\"\/><text x=\"645\" y=\"231\">3. call deploy hook<\/text>\n<\/g>\n<text x=\"730\" y=\"300\" fill=\"#7ee0a8\" font-size=\"13\" text-anchor=\"middle\">runs only if &#8220;test&#8221; passes<\/text>\n<\/g>\n<defs><marker id=\"dd-ar\" markerWidth=\"10\" markerHeight=\"10\" refX=\"8\" refY=\"5\" orient=\"auto\"><path d=\"M0 0 L10 5 L0 10 z\" fill=\"#7c5cff\"\/><\/marker><\/defs>\n<\/svg>\n<figcaption>Figure 1: An event starts a workflow. Each job gets a fresh machine and runs its steps in order. <code>needs<\/code> makes one job wait for another.<\/figcaption>\n<\/figure>\n\n<h2 id=\"sample-node-app\">The sample Node.js app<\/h2>\n<p>To keep the focus on the pipeline, we will use a tiny Node.js app with zero dependencies and the built-in test runner (<code>node --test<\/code>). Create this structure:<\/p>\n<pre><code>my-api\/\n\u251c\u2500\u2500 .github\/\n\u2502   \u2514\u2500\u2500 workflows\/\n\u2502       \u2514\u2500\u2500 ci.yml\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 math.js\n\u2502   \u251c\u2500\u2500 math.test.js\n\u2502   \u2514\u2500\u2500 server.js\n\u251c\u2500\u2500 Dockerfile\n\u251c\u2500\u2500 package.json\n\u2514\u2500\u2500 package-lock.json<\/code><\/pre>\n\n<span class=\"dd-file\">src\/math.js<\/span>\n<pre><code>export function add(a, b) {\n  if (typeof a !== 'number' || typeof b !== 'number') {\n    throw new TypeError('add() expects two numbers');\n  }\n  return a + b;\n}<\/code><\/pre>\n\n<span class=\"dd-file\">src\/math.test.js<\/span>\n<pre><code>import { test } from 'node:test';\nimport assert from 'node:assert\/strict';\nimport { add } from '.\/math.js';\n\ntest('adds two numbers', () =&gt; {\n  assert.equal(add(2, 3), 5);\n});\n\ntest('throws on bad input', () =&gt; {\n  assert.throws(() =&gt; add('2', 3), TypeError);\n});<\/code><\/pre>\n\n<span class=\"dd-file\">src\/server.js<\/span>\n<pre><code>import { createServer } from 'node:http';\nimport { add } from '.\/math.js';\n\nconst port = process.env.PORT || 3000;\n\ncreateServer((req, res) =&gt; {\n  res.setHeader('Content-Type', 'application\/json');\n  res.end(JSON.stringify({ ok: true, sum: add(2, 3) }));\n}).listen(port, () =&gt; console.log(`API running on port ${port}`));<\/code><\/pre>\n\n<span class=\"dd-file\">package.json<\/span>\n<pre><code>{\n  \"name\": \"my-api\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"engines\": { \"node\": \"&gt;=22\" },\n  \"scripts\": {\n    \"start\": \"node src\/server.js\",\n    \"test\": \"node --test\"\n  }\n}<\/code><\/pre>\n<p>Run <code>npm install<\/code> once so that <code>package-lock.json<\/code> is created, then <code>npm test<\/code> locally. You should see two passing tests. Commit everything, including the lock file \u2014 CI needs it.<\/p>\n<div class=\"dd-note\"><strong>Note:<\/strong> <code>node --test<\/code> automatically finds files named like <code>*.test.js<\/code>. No Jest or Mocha needed. If you are new to <code>async<\/code> code in tests, our guide on <a href=\"https:\/\/devdojo.co.in\/index.php\/2024\/08\/12\/mastering-javascript-promises-a-guide-to-asynchronous-programming\/\">JavaScript Promises<\/a> is a good refresher.<\/div>\n\n<h2 id=\"first-workflow\">Your first GitHub Actions CI\/CD workflow<\/h2>\n<p>Create the file <code>.github\/workflows\/ci.yml<\/code>. The folder name must be exactly <code>.github\/workflows<\/code> \u2014 otherwise GitHub ignores the file.<\/p>\n<span class=\"dd-file\">.github\/workflows\/ci.yml<\/span>\n<pre><code>name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the code\n        uses: actions\/checkout@v7\n\n      - name: Set up Node.js\n        uses: actions\/setup-node@v7\n        with:\n          node-version: 24\n          cache: npm\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run tests\n        run: npm test<\/code><\/pre>\n<p>Push this file and open the <strong>Actions<\/strong> tab of your repository. You will see the workflow run live, step by step. A green tick means every step exited with code 0; a red cross means some step failed, and you can click it to read the logs.<\/p>\n<p>Line by line:<\/p>\n<ul>\n<li><code>on<\/code> \u2014 run on pushes to <code>main<\/code> and on pull requests that target <code>main<\/code>.<\/li>\n<li><code>runs-on: ubuntu-latest<\/code> \u2014 use a fresh Linux virtual machine hosted by GitHub.<\/li>\n<li><code>actions\/checkout@v7<\/code> \u2014 download your repository code onto the runner. (Version 7 also refuses, by default, to check out code from forked pull requests in risky triggers like <code>pull_request_target<\/code> \u2014 a nice security upgrade.)<\/li>\n<li><code>actions\/setup-node@v7<\/code> \u2014 install Node.js 24 and cache npm&#8217;s download folder so later runs are faster.<\/li>\n<li><code>npm ci<\/code> \u2014 a clean, exact install from <code>package-lock.json<\/code>. It is faster and more predictable than <code>npm install<\/code> in CI.<\/li>\n<\/ul>\n<div class=\"dd-tip\"><strong>Tip:<\/strong> Add a status badge to your README so everyone can see if <code>main<\/code> is healthy: <code>![CI](https:\/\/github.com\/&lt;user&gt;\/&lt;repo&gt;\/actions\/workflows\/ci.yml\/badge.svg)<\/code><\/div>\n<\/div>\n\n\n<div class=\"dd-post\">\n<h2 id=\"triggers\">Choosing triggers: when should your workflow run?<\/h2>\n<p>The <code>on:<\/code> section decides when GitHub starts your workflow. Click each tab below to see the four triggers beginners use most.<\/p>\n<div class=\"dd-tabs\">\n<input type=\"radio\" name=\"dd-trig\" id=\"dd-t1\" checked>\n<input type=\"radio\" name=\"dd-trig\" id=\"dd-t2\">\n<input type=\"radio\" name=\"dd-trig\" id=\"dd-t3\">\n<input type=\"radio\" name=\"dd-trig\" id=\"dd-t4\">\n<div class=\"dd-tablist\">\n<label for=\"dd-t1\">push<\/label>\n<label for=\"dd-t2\">pull_request<\/label>\n<label for=\"dd-t3\">workflow_dispatch<\/label>\n<label for=\"dd-t4\">schedule<\/label>\n<\/div>\n<div class=\"dd-panel dd-p1\">\n<p><strong>Runs when commits land on a branch.<\/strong> Perfect for testing <code>main<\/code> and for deploying after a merge. You can also limit it to certain files with <code>paths<\/code>.<\/p>\n<pre><code>on:\n  push:\n    branches: [main]\n    paths: ['src\/**', 'package*.json']<\/code><\/pre>\n<\/div>\n<div class=\"dd-panel dd-p2\">\n<p><strong>Runs when a pull request is opened or updated.<\/strong> This is the heart of CI: you see test results right on the PR before merging. Secrets are not shared with PRs from forks, which keeps your keys safe.<\/p>\n<pre><code>on:\n  pull_request:\n    branches: [main]\n    types: [opened, synchronize, reopened]<\/code><\/pre>\n<\/div>\n<div class=\"dd-panel dd-p3\">\n<p><strong>Adds a &#8220;Run workflow&#8221; button<\/strong> in the Actions tab, so you can start it by hand \u2014 great for deployments or one-off jobs. You can even ask for inputs.<\/p>\n<pre><code>on:\n  workflow_dispatch:\n    inputs:\n      environment:\n        description: 'Where to deploy'\n        type: choice\n        options: [staging, production]\n        default: staging<\/code><\/pre>\n<\/div>\n<div class=\"dd-panel dd-p4\">\n<p><strong>Runs on a timer<\/strong> using cron syntax (always in UTC). Useful for nightly tests or dependency checks. 30 3 * * * means 03:30 UTC, which is 09:00 IST.<\/p>\n<pre><code>on:\n  schedule:\n    - cron: '30 3 * * *'<\/code><\/pre>\n<\/div>\n<\/div>\n\n<h2 id=\"matrix-cache\">Matrix, caching and speed<\/h2>\n<p>Real projects need a little more than the first workflow. Here is an improved <code>ci.yml<\/code> that tests on <strong>two Node.js LTS versions<\/strong> (22 and 24) in parallel, cancels outdated runs, limits permissions and adds a timeout.<\/p>\n<span class=\"dd-file\">.github\/workflows\/ci.yml (improved)<\/span>\n<pre><code>name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n\n# Only read access to the repo by default (safer)\npermissions:\n  contents: read\n\n# If you push again quickly, cancel the older run on the same branch\nconcurrency:\n  group: ci-${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  test:\n    name: Test on Node ${{ matrix.node-version }}\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    strategy:\n      fail-fast: false\n      matrix:\n        node-version: [22, 24]\n    steps:\n      - uses: actions\/checkout@v7\n\n      - uses: actions\/setup-node@v7\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: npm\n\n      - run: npm ci\n      - run: npm test<\/code><\/pre>\n<p>What each new part does:<\/p>\n<ul>\n<li><strong><code>strategy.matrix<\/code><\/strong> creates one job per value \u2014 here two jobs run side by side. <code>fail-fast: false<\/code> lets both finish even if one fails, so you see the full picture.<\/li>\n<li><strong><code>cache: npm<\/code><\/strong> stores npm&#8217;s download cache between runs, keyed on <code>package-lock.json<\/code>. When the lock file changes, the cache refreshes automatically.<\/li>\n<li><strong><code>permissions: contents: read<\/code><\/strong> limits what the automatic <code>GITHUB_TOKEN<\/code> can do. Give more only to the jobs that need it.<\/li>\n<li><strong><code>concurrency<\/code><\/strong> saves minutes: pushing three commits in a row will not run three full pipelines.<\/li>\n<li><strong><code>timeout-minutes<\/code><\/strong> stops a stuck job early (the default limit is 6 hours!).<\/li>\n<\/ul>\n<div class=\"dd-note\"><strong>Why Node 22 and 24?<\/strong> Both are long-term support (LTS) lines right now, while Node 26 is the newest &#8220;Current&#8221; release. Check the official <a href=\"https:\/\/nodejs.org\/en\/about\/previous-releases\" target=\"_blank\" rel=\"noopener\">Node.js release schedule<\/a> and update your matrix when versions change.<\/div>\n\n<h2 id=\"test-reports\">Saving test reports as artifacts<\/h2>\n<p>When a test fails, it helps to download a proper report. The Node.js test runner can print readable output to the log <em>and<\/em> write a JUnit XML file at the same time. Then <code>actions\/upload-artifact@v7<\/code> saves it on the run page.<\/p>\n<pre><code>      - name: Run tests with reports\n        run: &gt;\n          node --test\n          --test-reporter=spec --test-reporter-destination=stdout\n          --test-reporter=junit --test-reporter-destination=test-results.xml\n\n      - name: Upload test report\n        if: always()\n        uses: actions\/upload-artifact@v7\n        with:\n          name: test-results-node-${{ matrix.node-version }}\n          path: test-results.xml\n          retention-days: 7<\/code><\/pre>\n<p><code>if: always()<\/code> is important: without it, the upload step is skipped when tests fail \u2014 exactly when you need the report most. Each matrix job uses a different artifact <code>name<\/code>, because two artifacts in the same run cannot share a name.<\/p>\n\n<figure>\n<svg viewBox=\"0 0 900 300\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"dd-f2t\">\n<title id=\"dd-f2t\">Timeline comparing a slow pipeline and a fast pipeline with matrix, cache and concurrency<\/title>\n<rect width=\"900\" height=\"300\" rx=\"14\" fill=\"#0f1430\"\/>\n<g font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"14\">\n<text x=\"30\" y=\"45\" fill=\"#fff\" font-size=\"17\" font-weight=\"700\">Before: one long job, no cache<\/text>\n<rect x=\"30\" y=\"60\" width=\"210\" height=\"34\" rx=\"6\" fill=\"#3a2f6b\"\/><text x=\"45\" y=\"82\" fill=\"#e6e9f5\">npm install (no cache)<\/text>\n<rect x=\"240\" y=\"60\" width=\"160\" height=\"34\" rx=\"6\" fill=\"#2b3a6b\"\/><text x=\"255\" y=\"82\" fill=\"#e6e9f5\">test Node 22<\/text>\n<rect x=\"400\" y=\"60\" width=\"160\" height=\"34\" rx=\"6\" fill=\"#2b3a6b\"\/><text x=\"415\" y=\"82\" fill=\"#e6e9f5\">test Node 24<\/text>\n<text x=\"575\" y=\"82\" fill=\"#ffb4a8\" font-weight=\"700\">\u2248 slow, sequential<\/text>\n<text x=\"30\" y=\"150\" fill=\"#fff\" font-size=\"17\" font-weight=\"700\">After: matrix + cache + concurrency<\/text>\n<rect x=\"30\" y=\"165\" width=\"90\" height=\"34\" rx=\"6\" fill=\"#1d5a45\"\/><text x=\"40\" y=\"187\" fill=\"#e6e9f5\">npm ci \u26a1<\/text>\n<rect x=\"120\" y=\"165\" width=\"160\" height=\"34\" rx=\"6\" fill=\"#2b3a6b\"\/><text x=\"135\" y=\"187\" fill=\"#e6e9f5\">test Node 22<\/text>\n<rect x=\"30\" y=\"210\" width=\"90\" height=\"34\" rx=\"6\" fill=\"#1d5a45\"\/><text x=\"40\" y=\"232\" fill=\"#e6e9f5\">npm ci \u26a1<\/text>\n<rect x=\"120\" y=\"210\" width=\"160\" height=\"34\" rx=\"6\" fill=\"#2b3a6b\"\/><text x=\"135\" y=\"232\" fill=\"#e6e9f5\">test Node 24<\/text>\n<text x=\"300\" y=\"210\" fill=\"#7ee0a8\" font-weight=\"700\">\u2248 parallel jobs, cached downloads<\/text>\n<path d=\"M30 270 H870\" stroke=\"#3178c6\" stroke-width=\"2\"\/><text x=\"840\" y=\"290\" fill=\"#9aa6d8\" text-anchor=\"end\">time \u2192<\/text>\n<\/g>\n<\/svg>\n<figcaption>Figure 2: A matrix runs versions in parallel and the npm cache shortens installs, so feedback arrives much sooner.<\/figcaption>\n<\/figure>\n\n<h2 id=\"docker-ghcr\">Build and push a Docker image to GHCR<\/h2>\n<p>Now the &#8220;CD&#8221; part. A very common pattern is to package the app as a Docker image and publish it to <strong>GitHub Container Registry (GHCR)<\/strong> whenever you create a version tag like <code>v1.2.0<\/code>. First, a small Dockerfile:<\/p>\n<span class=\"dd-file\">Dockerfile<\/span>\n<pre><code>FROM node:24-alpine\nWORKDIR \/app\nCOPY package*.json .\/\nRUN npm ci --omit=dev\nCOPY src .\/src\nENV NODE_ENV=production\nEXPOSE 3000\nUSER node\nCMD [\"node\", \"src\/server.js\"]<\/code><\/pre>\n<p>Then a separate release workflow using Docker&#8217;s official actions:<\/p>\n<span class=\"dd-file\">.github\/workflows\/release.yml<\/span>\n<pre><code>name: Release image\n\non:\n  push:\n    tags: ['v*']\n\npermissions:\n  contents: read\n  packages: write   # needed to push to ghcr.io\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v7\n\n      - uses: docker\/setup-buildx-action@v4\n\n      - name: Log in to GHCR\n        uses: docker\/login-action@v4\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Create tags and labels\n        id: meta\n        uses: docker\/metadata-action@v6\n        with:\n          images: ghcr.io\/${{ github.repository }}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=sha\n\n      - name: Build and push\n        uses: docker\/build-push-action@v7\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max<\/code><\/pre>\n<p>To release, run <code>git tag v1.0.0 &amp;&amp; git push origin v1.0.0<\/code>. The metadata action turns that tag into image tags like <code>1.0.0<\/code>, <code>1.0<\/code> and <code>sha-abc1234<\/code>, and it also makes the image name lowercase (registries reject capital letters). <code>cache-from\/cache-to: type=gha<\/code> reuses Docker layers between runs, so rebuilds take seconds when only your code changed.<\/p>\n<div class=\"dd-tip\"><strong>Tip:<\/strong> You don&#8217;t need to create any secret for GHCR. <code>secrets.GITHUB_TOKEN<\/code> is created automatically for every run \u2014 you only have to allow <code>packages: write<\/code>.<\/div>\n<\/div>\n\n\n<div class=\"dd-post\">\n<h2 id=\"deploy-secrets\">Deploy with secrets and environments<\/h2>\n<p>Most hosting platforms (Render, Railway, Coolify, your own VPS script and many more) give you a <strong>deploy hook<\/strong>: a secret URL that starts a new deployment when you call it. Let&#8217;s add a <code>deploy<\/code> job to <code>ci.yml<\/code> that runs only after tests pass on <code>main<\/code>.<\/p>\n<p><strong>Step 1 \u2014 store the secret.<\/strong> In your repo go to <em>Settings \u2192 Environments \u2192 New environment<\/em>, name it <code>production<\/code>, and add a secret called <code>DEPLOY_HOOK_URL<\/code>. In the same screen you can turn on <em>Required reviewers<\/em>, so a human must approve every production deploy.<\/p>\n<p><strong>Step 2 \u2014 add the job:<\/strong><\/p>\n<span class=\"dd-file\">.github\/workflows\/ci.yml (add below the test job)<\/span>\n<pre><code>  deploy:\n    needs: test\n    if: github.event_name == 'push' &amp;&amp; github.ref == 'refs\/heads\/main'\n    runs-on: ubuntu-latest\n    environment:\n      name: production\n      url: https:\/\/my-api.example.com\n    steps:\n      - name: Trigger deployment\n        env:\n          DEPLOY_HOOK_URL: ${{ secrets.DEPLOY_HOOK_URL }}\n        run: curl --fail --silent --show-error -X POST \"$DEPLOY_HOOK_URL\"\n\n      - name: Smoke test\n        run: |\n          sleep 30\n          curl --fail --retry 5 --retry-delay 10 https:\/\/my-api.example.com\/<\/code><\/pre>\n<ul>\n<li><code>needs: test<\/code> \u2014 waits for <em>all<\/em> matrix test jobs to succeed.<\/li>\n<li><code>if:<\/code> \u2014 skips deploys for pull requests and other branches.<\/li>\n<li><code>environment: production<\/code> \u2014 unlocks that environment&#8217;s secrets and applies its protection rules.<\/li>\n<li>Passing the secret through <code>env<\/code> (not pasting <code>${{ }}<\/code> directly inside the command) is a safer habit, and GitHub hides secret values in logs automatically.<\/li>\n<li><code>curl --fail<\/code> makes the step fail on HTTP errors instead of silently &#8220;passing&#8221;.<\/li>\n<\/ul>\n<div class=\"dd-warn\"><strong>Careful:<\/strong> never write secrets into your YAML file or <code>echo<\/code> them for debugging. Anyone who can read the repo \u2014 or the logs \u2014 could steal them. For cloud providers like AWS, Azure or Google Cloud, prefer <strong>OpenID Connect (OIDC)<\/strong> with <code>permissions: id-token: write<\/code>, which gives short-lived credentials instead of long-lived keys. See GitHub&#8217;s official <a href=\"https:\/\/docs.github.com\/en\/actions\/reference\/security\/secure-use\" target=\"_blank\" rel=\"noopener\">secure use guide<\/a>.<\/div>\n\n<h3>Bonus: reuse the setup with a composite action<\/h3>\n<p>If several workflows repeat &#8220;checkout + setup Node + npm ci&#8221;, move those steps into a small local action:<\/p>\n<span class=\"dd-file\">.github\/actions\/setup-project\/action.yml<\/span>\n<pre><code>name: Setup project\ndescription: Install Node.js and dependencies\ninputs:\n  node-version:\n    description: Node.js version\n    default: '24'\nruns:\n  using: composite\n  steps:\n    - uses: actions\/setup-node@v7\n      with:\n        node-version: ${{ inputs.node-version }}\n        cache: npm\n    - run: npm ci\n      shell: bash<\/code><\/pre>\n<p>Use it in any job after checkout with <code>- uses: .\/.github\/actions\/setup-project<\/code>. Note that <code>run<\/code> steps inside a composite action must declare <code>shell<\/code>.<\/p>\n\n<h2 id=\"errors\">Common errors and fixes<\/h2>\n<table>\n<thead><tr><th>Error message (or symptom)<\/th><th>Why it happens<\/th><th>How to fix it<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>Workflow never starts<\/td><td>File is not in <code>.github\/workflows\/<\/code>, or the branch name in <code>on:<\/code> doesn&#8217;t match (e.g. <code>master<\/code> vs <code>main<\/code>).<\/td><td>Check the folder path and branch filters. YAML files must end in <code>.yml<\/code> or <code>.yaml<\/code>.<\/td><\/tr>\n<tr><td><code>Invalid workflow file<\/code><\/td><td>Wrong indentation or tabs in YAML.<\/td><td>Use 2 spaces, never tabs. The Actions tab shows the exact line number.<\/td><\/tr>\n<tr><td><code>npm ci<\/code> fails: &#8220;can only install with an existing package-lock.json&#8221;<\/td><td>The lock file was not committed (or is in <code>.gitignore<\/code>).<\/td><td>Run <code>npm install<\/code> locally and commit <code>package-lock.json<\/code>.<\/td><\/tr>\n<tr><td><code>Dependencies lock file is not found<\/code><\/td><td><code>cache: npm<\/code> could not find a lock file in the repo root.<\/td><td>Commit the lock file, or set <code>cache-dependency-path: app\/package-lock.json<\/code> for subfolders.<\/td><\/tr>\n<tr><td><code>Resource not accessible by integration<\/code><\/td><td><code>GITHUB_TOKEN<\/code> lacks a permission (e.g. writing PR comments or packages).<\/td><td>Add only the needed scope, such as <code>pull-requests: write<\/code> or <code>packages: write<\/code>.<\/td><\/tr>\n<tr><td><code>denied: permission_denied<\/code> when pushing to ghcr.io<\/td><td>Missing <code>packages: write<\/code>, or the package belongs to another repo.<\/td><td>Add the permission; in the package settings give this repo &#8220;Write&#8221; access.<\/td><\/tr>\n<tr><td><code>repository name must be lowercase<\/code><\/td><td>Your GitHub username or repo has capital letters.<\/td><td>Use <code>docker\/metadata-action<\/code> (it lowercases names) as shown above.<\/td><\/tr>\n<tr><td>Secret is empty in a PR<\/td><td>Secrets are not passed to workflows triggered by pull requests from forks.<\/td><td>Expected and safe. Run secret-needing steps only on <code>push<\/code> to <code>main<\/code>.<\/td><\/tr>\n<tr><td>Tests pass locally, fail in CI<\/td><td>Different Node version, timezone, or a missing environment variable.<\/td><td>Match versions with a matrix or <code>node-version-file: .nvmrc<\/code>; set needed <code>env<\/code> values in the job.<\/td><\/tr>\n<\/tbody>\n<\/table>\n\n<h2 id=\"best-practices\">Best practices for GitHub Actions CI\/CD<\/h2>\n<ol>\n<li><strong>Keep CI fast.<\/strong> Aim for under 5 minutes. Use caching, a matrix, and <code>concurrency<\/code> to cancel stale runs.<\/li>\n<li><strong>Use least privilege.<\/strong> Set <code>permissions: contents: read<\/code> at the top and raise it per job only where needed.<\/li>\n<li><strong>Pin action versions.<\/strong> Use at least a major tag (<code>@v7<\/code>). For high-security repos, pin to a full commit SHA and let Dependabot update it.<\/li>\n<li><strong>Let Dependabot update actions.<\/strong> Add a <code>.github\/dependabot.yml<\/code> entry with <code>package-ecosystem: github-actions<\/code> so you hear about new major versions.<\/li>\n<li><strong>Protect your main branch.<\/strong> In branch rules, require the CI checks to pass before a PR can be merged.<\/li>\n<li><strong>Separate CI from CD.<\/strong> Tests run on every PR; deploys run only from <code>main<\/code> or tags, through a protected environment.<\/li>\n<li><strong>Add timeouts.<\/strong> <code>timeout-minutes<\/code> on every job avoids burning hours of minutes on a hung process.<\/li>\n<li><strong>Make failures readable.<\/strong> Name your steps clearly and upload reports as artifacts.<\/li>\n<\/ol>\n<span class=\"dd-file\">.github\/dependabot.yml<\/span>\n<pre><code>version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \/\n    schedule:\n      interval: weekly\n  - package-ecosystem: npm\n    directory: \/\n    schedule:\n      interval: weekly<\/code><\/pre>\n<p>Building AI tools in Node.js? The same pipeline works great for the server we built in <a href=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/03\/build-mcp-server-typescript\/\">How to Build an MCP Server in TypeScript<\/a> \u2014 just add a <code>npm run build<\/code> step before the tests.<\/p>\n\n<h2 id=\"faq\">FAQ<\/h2>\n<details><summary>Is GitHub Actions free?<\/summary><p>Yes for public repositories using standard GitHub-hosted runners. Private repositories get a monthly allowance of free minutes and storage depending on your plan; after that, usage is billed. Linux runners use the fewest minutes, so prefer <code>ubuntu-latest<\/code> unless you need Windows or macOS.<\/p><\/details>\n<details><summary>What is the difference between <code>npm ci<\/code> and <code>npm install<\/code>?<\/summary><p><code>npm ci<\/code> installs exactly what is in <code>package-lock.json<\/code>, deletes any existing <code>node_modules<\/code> first and fails if the lock file and <code>package.json<\/code> disagree. That makes builds repeatable, which is what you want in CI.<\/p><\/details>\n<details><summary>Can I run GitHub Actions on my own server?<\/summary><p>Yes. You can register a <em>self-hosted runner<\/em> (a small agent program) on your own machine and use <code>runs-on: self-hosted<\/code>. Be careful with public repos: anyone opening a PR could run code on your machine.<\/p><\/details>\n<details><summary>How do I test a workflow without pushing many commits?<\/summary><p>Add <code>workflow_dispatch<\/code> so you can run it from the Actions tab, work on a separate branch, or use the open-source tool <code>act<\/code> to run workflows locally in Docker.<\/p><\/details>\n<details><summary>Should I use <code>ubuntu-latest<\/code> or a fixed version?<\/summary><p><code>ubuntu-latest<\/code> is fine for most projects. If you need total stability, pin a version like <code>ubuntu-24.04<\/code> and upgrade on your own schedule.<\/p><\/details>\n<details><summary>Is GitHub Actions good for Indian startups and freelancers?<\/summary><p>Very much so. It needs no extra server or paid CI tool, the free tier covers most small projects, and the same skills are listed in many DevOps and full-stack job descriptions.<\/p><\/details>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>You now have a complete <strong>GitHub Actions CI\/CD<\/strong> pipeline for Node.js: tests on every pull request across two LTS versions, cached installs, downloadable test reports, a Docker image published to GHCR on every version tag, and a protected production deploy. Start with the 10-line workflow, get it green, then add one improvement at a time. Small, steady upgrades beat a giant YAML file you don&#8217;t understand.<\/p>\n<p>Your next step: open your favourite side project, add <code>.github\/workflows\/ci.yml<\/code>, and push. In two minutes you will have your first green tick.<\/p>\n<div class=\"dd-cta\">\n<h3>New posts every day on DevDojo<\/h3>\n<p>Practical guides on AI, frontend, backend and DevOps \u2014 written for real developers. Bookmark <a href=\"https:\/\/devdojo.co.in\/\">devdojo.co.in<\/a> and come back tomorrow, or drop your CI questions in the comments below.<\/p>\n<\/div>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.<\/p>\n","protected":false},"author":1,"featured_media":175,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7,57,8],"tags":[59,61,60,58,43],"class_list":["post-176","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-backend","category-devops","category-nodejs","tag-ci-cd","tag-devops","tag-docker","tag-github-actions","tag-node-js"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.2.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Lokendra\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.2.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"DevDojo - Empowering Developers\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"GitHub Actions CI\/CD for Node.js: Beginner&#039;s Guide\" \/>\n\t\t<meta property=\"og:description\" content=\"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-10-06T13:45:10+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-10-06T13:45:11+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n\t\t<meta name=\"twitter:title\" content=\"GitHub Actions CI\/CD for Node.js: Beginner&#039;s Guide\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BlogPosting\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#blogposting\",\"name\":\"GitHub Actions CI\\\/CD for Node.js: Beginner's Guide\",\"headline\":\"GitHub Actions CI\\\/CD for Node.js: A Complete Beginner&#8217;s Guide\",\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/github-actions-ci-cd-nodejs.jpg\",\"width\":1200,\"height\":630,\"caption\":\"GitHub Actions CI\\\/CD for Node.js pipeline: push, install, test, build and deploy\"},\"datePublished\":\"2026-10-06T13:45:10+00:00\",\"dateModified\":\"2026-10-06T13:45:11+00:00\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#webpage\"},\"articleSection\":\"Backend, DevOps, NodeJS, CI\\\/CD, DevOps, Docker, GitHub Actions, Node.js\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"name\":\"Backend\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"position\":2,\"name\":\"Backend\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/nodejs\\\/#listItem\",\"name\":\"NodeJS\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/nodejs\\\/#listItem\",\"position\":3,\"name\":\"NodeJS\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/nodejs\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#listItem\",\"name\":\"GitHub Actions CI\\\/CD for Node.js: A Complete Beginner&#8217;s Guide\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"name\":\"Backend\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#listItem\",\"position\":4,\"name\":\"GitHub Actions CI\\\/CD for Node.js: A Complete Beginner&#8217;s Guide\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/nodejs\\\/#listItem\",\"name\":\"NodeJS\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/\",\"name\":\"Lokendra\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#authorImage\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g\",\"width\":96,\"height\":96,\"caption\":\"Lokendra\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#webpage\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/\",\"name\":\"GitHub Actions CI\\\/CD for Node.js: Beginner's Guide\",\"description\":\"Learn GitHub Actions CI\\\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/github-actions-ci-cd-nodejs.jpg\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#mainImage\",\"width\":1200,\"height\":630,\"caption\":\"GitHub Actions CI\\\/CD for Node.js pipeline: push, install, test, build and deploy\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/06\\\/github-actions-ci-cd-nodejs\\\/#mainImage\"},\"datePublished\":\"2026-10-06T13:45:10+00:00\",\"dateModified\":\"2026-10-06T13:45:11+00:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"GitHub Actions CI\/CD for Node.js: Beginner's Guide","description":"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.","canonical_url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BlogPosting","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#blogposting","name":"GitHub Actions CI\/CD for Node.js: Beginner's Guide","headline":"GitHub Actions CI\/CD for Node.js: A Complete Beginner&#8217;s Guide","author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/10\/github-actions-ci-cd-nodejs.jpg","width":1200,"height":630,"caption":"GitHub Actions CI\/CD for Node.js pipeline: push, install, test, build and deploy"},"datePublished":"2026-10-06T13:45:10+00:00","dateModified":"2026-10-06T13:45:11+00:00","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#webpage"},"isPartOf":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#webpage"},"articleSection":"Backend, DevOps, NodeJS, CI\/CD, DevOps, Docker, GitHub Actions, Node.js"},{"@type":"BreadcrumbList","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","position":1,"name":"Home","item":"https:\/\/devdojo.co.in\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","name":"Backend"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","position":2,"name":"Backend","item":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/#listItem","name":"NodeJS"},"previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/#listItem","position":3,"name":"NodeJS","item":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#listItem","name":"GitHub Actions CI\/CD for Node.js: A Complete Beginner&#8217;s Guide"},"previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","name":"Backend"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#listItem","position":4,"name":"GitHub Actions CI\/CD for Node.js: A Complete Beginner&#8217;s Guide","previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/#listItem","name":"NodeJS"}}]},{"@type":"Organization","@id":"https:\/\/devdojo.co.in\/#organization","name":"DevDojo","description":"Empowering Developers","url":"https:\/\/devdojo.co.in\/"},{"@type":"Person","@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author","url":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/","name":"Lokendra","image":{"@type":"ImageObject","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g","width":96,"height":96,"caption":"Lokendra"}},{"@type":"WebPage","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#webpage","url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/","name":"GitHub Actions CI\/CD for Node.js: Beginner's Guide","description":"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/devdojo.co.in\/#website"},"breadcrumb":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#breadcrumblist"},"author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"creator":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/10\/github-actions-ci-cd-nodejs.jpg","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#mainImage","width":1200,"height":630,"caption":"GitHub Actions CI\/CD for Node.js pipeline: push, install, test, build and deploy"},"primaryImageOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/#mainImage"},"datePublished":"2026-10-06T13:45:10+00:00","dateModified":"2026-10-06T13:45:11+00:00"},{"@type":"WebSite","@id":"https:\/\/devdojo.co.in\/#website","url":"https:\/\/devdojo.co.in\/","name":"DevDojo","description":"Empowering Developers","inLanguage":"en-US","publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"}}]},"og:locale":"en_US","og:site_name":"DevDojo - Empowering Developers","og:type":"article","og:title":"GitHub Actions CI\/CD for Node.js: Beginner's Guide","og:description":"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.","og:url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/","article:published_time":"2026-10-06T13:45:10+00:00","article:modified_time":"2026-10-06T13:45:11+00:00","twitter:card":"summary_large_image","twitter:title":"GitHub Actions CI\/CD for Node.js: Beginner's Guide","twitter:description":"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely."},"aioseo_meta_data":{"post_id":"176","title":"GitHub Actions CI\/CD for Node.js: Beginner's Guide","description":"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.","keywords":null,"keyphrases":{"focus":{"keyphrase":"GitHub Actions CI\/CD"},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":"GitHub Actions CI\/CD for Node.js: Beginner's Guide","og_description":"Learn GitHub Actions CI\/CD for Node.js step by step: run tests on every push, cache npm, use a version matrix, push Docker images to GHCR and deploy safely.","og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"BlogPosting","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":0,"frequency":"default","local_seo":null,"breadcrumb_settings":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-10-06 12:58:32","updated":"2026-10-06 13:45:11","focus_keyword":"GitHub Actions CI\/CD","additional_keywords":null,"truseo_locale":null,"seo_analyzer_scan_date":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/index.php\/category\/backend\/\" title=\"Backend\">Backend<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/\" title=\"NodeJS\">NodeJS<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tGitHub Actions CI\/CD for Node.js: A Complete Beginner\u2019s Guide\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/devdojo.co.in\/"},{"label":"Backend","link":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/"},{"label":"NodeJS","link":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/"},{"label":"GitHub Actions CI\/CD for Node.js: A Complete Beginner&#8217;s Guide","link":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/06\/github-actions-ci-cd-nodejs\/"}],"_links":{"self":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/176","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/comments?post=176"}],"version-history":[{"count":1,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/176\/revisions"}],"predecessor-version":[{"id":177,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/176\/revisions\/177"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media\/175"}],"wp:attachment":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media?parent=176"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/categories?post=176"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/tags?post=176"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}