{"id":1767,"date":"2026-10-22T06:11:21","date_gmt":"2026-10-22T06:11:21","guid":{"rendered":"https:\/\/devopsschool.org\/blog\/?p=1767"},"modified":"2026-10-22T06:11:22","modified_gmt":"2026-10-22T06:11:22","slug":"how-to-build-your-first-pipeline-from-scratch-a-step-by-step-ci-cd-guide","status":"publish","type":"post","link":"https:\/\/devopsschool.org\/blog\/how-to-build-your-first-pipeline-from-scratch-a-step-by-step-ci-cd-guide\/","title":{"rendered":"How to Build Your First Pipeline From Scratch: A Step-by-Step CI\/CD Guide"},"content":{"rendered":"\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"572\" src=\"https:\/\/devopsschool.org\/blog\/wp-content\/uploads\/2026\/10\/image-12.png\" alt=\"\" class=\"wp-image-1768\" srcset=\"https:\/\/devopsschool.org\/blog\/wp-content\/uploads\/2026\/10\/image-12.png 1024w, https:\/\/devopsschool.org\/blog\/wp-content\/uploads\/2026\/10\/image-12-300x168.png 300w, https:\/\/devopsschool.org\/blog\/wp-content\/uploads\/2026\/10\/image-12-768x429.png 768w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Every production issue that stems from an untested code change, a forgotten build step, or a missed lint error shares a common origin: manual execution. When steps rely on an engineer remembering to run them locally before pushing code, mistakes happen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An automated pipeline replaces manual routine with deterministic execution. At its core, a delivery pipeline is a script triggered by repository events\u2014such as a commit or pull request\u2014that builds your software in a fresh environment, runs automated test suites, and flags regressions before code merges into your primary branch.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">By the end of this guide, you will have a working continuous integration (CI) pipeline running from scratch using <strong>GitHub Actions<\/strong> and a lightweight Node.js project. You will understand how code triggers a runner, how jobs and steps execute, how to parse logs to diagnose failures, and how to verify pipeline health reliably.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Core Pipeline Concepts: How Automation Works Under the Hood<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before writing configuration files, it helps to establish a mental model of what happens when a continuous integration engine runs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Plaintext<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#091;Developer Push \/ PR] \n        \u2502\n        \u25bc (Event Trigger)\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502             Workflow \/ Pipeline          \u2502\n\u2502                                          \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  \u2502\n\u2502  \u2502 Job: Build &amp; Validate (Fresh VM)   \u2502  \u2502\n\u2502  \u2502                                    \u2502  \u2502\n\u2502  \u2502  1. Check out repository code      \u2502  \u2502\n\u2502  \u2502  2. Install language runtime       \u2502  \u2502\n\u2502  \u2502  3. Install project dependencies   \u2502  \u2502\n\u2502  \u2502  4. Run code syntax \/ linter       \u2502  \u2502\n\u2502  \u2502  5. Execute test suite             \u2502  \u2502\n\u2502  \u2502  6. Package build artifact         \u2502  \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518  \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n        \u2502\n   \u250c\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n   \u25bc                             \u25bc\n&#091;Exit Code 0: PASS]          &#091;Exit Code &gt;0: FAIL]\nMerge Allowed \/ Notify       Block Merge \/ Inspect Logs\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">1. Workflows, Jobs, and Steps<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Workflow (or Pipeline):<\/strong> The top-level automated process defined in your repository. It specifies <em>when<\/em> the automation should run (triggers) and <em>what<\/em> it should execute.<\/li>\n\n\n\n<li><strong>Jobs:<\/strong> An isolated set of actions executed on a single host or container (known as a <em>runner<\/em>). By default, independent jobs run concurrently. If a job depends on the output of another, they are configured sequentially.<\/li>\n\n\n\n<li><strong>Steps:<\/strong> The individual tasks inside a job. Steps execute sequentially on the same runner. If any step fails, subsequent steps are skipped by default, and the entire job terminates with a failed status.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">2. Triggers (Events)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A pipeline does not run continuously in an infinite loop; it responds to declarative event hooks. Common triggers include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>push<\/code>: Whenever commits are sent to a branch.<\/li>\n\n\n\n<li><code>pull_request<\/code>: When a branch comparison is opened or updated.<\/li>\n\n\n\n<li><code>workflow_dispatch<\/code>: A manual trigger fired from the web interface or API.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">3. Exit Codes and Pipeline State<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Automated pipelines rely on Unix process exit codes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>An exit code of <code>0<\/code> denotes success.<\/li>\n\n\n\n<li>Any non-zero exit code (e.g., <code>1<\/code>, <code>127<\/code>) signals a failure.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">When an automated test framework or build tool encounters an error, it exits with a non-zero code. The pipeline runner intercepts this code, halts downstream execution, marks the stage with a red failure indicator, and prevents pull requests from merging.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. CI vs. CD<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Continuous Integration (CI):<\/strong> Focuses on validating changes. It checks out code, installs dependencies, validates syntax, compiles binaries (if applicable), and runs tests. Its objective is early regression detection.<\/li>\n\n\n\n<li><strong>Continuous Delivery \/ Deployment (CD):<\/strong> Takes the validated output (an artifact, binary, or container image) and releases it to staging or production environments.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">A reliable CI pipeline is a mandatory prerequisite for CD. In this tutorial, we build a foundational CI pipeline.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites and Tech Stack Selection<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To keep the pipeline fast, zero-cost, and easy to run without provisioning external servers, we will use:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>GitHub Actions:<\/strong> It requires no self-hosted infrastructure. Free-tier accounts include generous monthly runner minutes, and workflow definitions live directly inside the repository as YAML files.<\/li>\n\n\n\n<li><strong>Node.js (LTS):<\/strong> JavaScript\/TypeScript has zero-dependency runtime requirements, clear package management via <code>npm<\/code>, and readable testing libraries.<\/li>\n\n\n\n<li><strong>Git and a GitHub Account:<\/strong> You need local Git installed and an authenticated GitHub account to push repositories.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">No cloud provider accounts (AWS\/GCP\/Azure), external hosting platforms, or credit cards are required.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Initialize the Application Project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A pipeline needs code to evaluate. We will create a minimal Node.js utility containing a functional module and an automated test suite.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Initialize the Directory and Manifest<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create an empty directory on your local machine and initialize an <code>npm<\/code> package:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir first-pipeline-project\ncd first-pipeline-project\nnpm init -y\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Open <code>package.json<\/code> and replace its contents with the following:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">JSON<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"name\": \"first-pipeline-project\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Demonstration project for learning CI\/CD pipelines\",\n  \"main\": \"src\/calculator.js\",\n  \"scripts\": {\n    \"test\": \"node --test test\/calculator.test.js\",\n    \"lint\": \"node -c src\/calculator.js test\/calculator.test.js\"\n  },\n  \"keywords\": &#091;],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Notice the two scripts:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>lint<\/code>: Uses the Node.js compiler check (<code>node -c<\/code>) to validate JavaScript syntax without executing the files.<\/li>\n\n\n\n<li><code>test<\/code>: Uses Node\u2019s native, zero-dependency test runner (<code>node --test<\/code>), introduced in Node 18+.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">2. Add the Application Logic<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create a <code>src\/<\/code> directory and add <code>calculator.js<\/code>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir src test\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Paste the following logic into <code>src\/calculator.js<\/code>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">JavaScript<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/**\n * Simple calculation utility functions.\n *\/\nfunction add(a, b) {\n  if (typeof a !== 'number' || typeof b !== 'number') {\n    throw new TypeError('Both arguments must be numbers');\n  }\n  return a + b;\n}\n\nfunction subtract(a, b) {\n  if (typeof a !== 'number' || typeof b !== 'number') {\n    throw new TypeError('Both arguments must be numbers');\n  }\n  return a - b;\n}\n\nmodule.exports = { add, subtract };\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. Add the Automated Test Suite<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Open <code>test\/calculator.test.js<\/code> and add unit tests that verify both happy paths and error handling:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">JavaScript<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const test = require('node:test');\nconst assert = require('node:assert\/strict');\nconst { add, subtract } = require('..\/src\/calculator');\n\ntest('add() correctly sums two numbers', () =&gt; {\n  assert.equal(add(2, 3), 5);\n  assert.equal(add(-1, 1), 0);\n});\n\ntest('add() throws an error when given non-numeric inputs', () =&gt; {\n  assert.throws(() =&gt; add('2', 3), {\n    name: 'TypeError',\n    message: 'Both arguments must be numbers',\n  });\n});\n\ntest('subtract() correctly deducts second number from first', () =&gt; {\n  assert.equal(subtract(10, 4), 6);\n  assert.equal(subtract(0, 5), -5);\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4. Verify Local Execution<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Always verify that your validation commands function on your local workstation before automating them in a pipeline. Run:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm run lint\nnpm test\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected output:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Plaintext<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\u2714 add() correctly sums two numbers (0.8ms)\n\u2714 add() throws an error when given non-numeric inputs (0.2ms)\n\u2714 subtract() correctly deducts second number from first (0.2ms)\n\u2139 tests 3\n\u2139 suites 0\n\u2139 pass 3\n\u2139 fail 0\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Establish Source Control<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pipelines exist in context with version control. Initialize your local Git repository and create a <code>.gitignore<\/code> file to ensure untracked build artifacts and dependencies are not committed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>.gitignore<\/code>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Plaintext<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node_modules\/\n.DS_Store\n*.log\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Commit the codebase locally:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git init\ngit add .\ngit commit -m \"feat: initial project structure with calculator module and tests\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Next, create a new <strong>public or private<\/strong> repository on GitHub named <code>first-pipeline-project<\/code>, connect your remote origin, and push:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git branch -M main\ngit remote add origin https:\/\/github.com\/&lt;YOUR-USERNAME&gt;\/first-pipeline-project.git\ngit push -u origin main\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Write the Pipeline Configuration File<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub Actions looks for workflow definitions in a dedicated directory: <code>.github\/workflows\/<\/code>. Configuration files must use YAML syntax (<code>.yml<\/code> or <code>.yaml<\/code>).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Create the Directory and File<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p .github\/workflows\ntouch .github\/workflows\/ci.yml\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. Add the Pipeline Specification<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Add the following configuration to <code>.github\/workflows\/ci.yml<\/code>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>name: Continuous Integration\n\non:\n  push:\n    branches: &#091; main ]\n  pull_request:\n    branches: &#091; main ]\n\npermissions:\n  contents: read\n\njobs:\n  validate-and-test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out repository code\n        uses: actions\/checkout@v4\n\n      - name: Set up Node.js runtime\n        uses: actions\/setup-node@v4\n        with:\n          node-version: '20'\n          cache: 'npm'\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Validate code syntax (Lint)\n        run: npm run lint\n\n      - name: Execute automated unit tests\n        run: npm test\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Deconstruct the Configuration File<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To manage and debug pipelines effectively, you need to understand each line of the configuration:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><td><strong>Section \/ Key<\/strong><\/td><td><strong>Purpose<\/strong><\/td><td><strong>Technical Explanation<\/strong><\/td><\/tr><\/thead><tbody><tr><td><code>name<\/code><\/td><td>Human-readable pipeline label<\/td><td>Displayed in GitHub\u2019s &#8220;Actions&#8221; tab UI.<\/td><\/tr><tr><td><code>on<\/code><\/td><td>Event triggers<\/td><td>Specifies that this workflow runs whenever commits are pushed directly to <code>main<\/code> or when a Pull Request targets <code>main<\/code>.<\/td><\/tr><tr><td><code>permissions<\/code><\/td><td>Security boundary<\/td><td>Enforces the Principle of Least Privilege. Setting <code>contents: read<\/code> prevents the pipeline runner from modifying code or writing tags unexpectedly.<\/td><\/tr><tr><td><code>jobs<\/code><\/td><td>Execution containers<\/td><td>Defines one or more discrete jobs. Here, we declare a single job named <code>validate-and-test<\/code>.<\/td><\/tr><tr><td><code>runs-on: ubuntu-latest<\/code><\/td><td>Virtual machine host<\/td><td>Instructs GitHub to provision an ephemeral, cleanly imaged Ubuntu Linux runner for this job execution.<\/td><\/tr><tr><td><code>actions\/checkout@v4<\/code><\/td><td>Source checkout step<\/td><td>Clones your Git repository onto the runner&#8217;s filesystem so downstream tasks can access files.<\/td><\/tr><tr><td><code>actions\/setup-node@v4<\/code><\/td><td>Language runtime<\/td><td>Downloads and configures the Node.js v20 engine into the runner&#8217;s <code>$PATH<\/code>. The <code>cache: 'npm'<\/code> parameter speeds up execution by caching package archives across runs.<\/td><\/tr><tr><td><code>run: npm ci<\/code><\/td><td>Dependency installation<\/td><td>Uses clean install (<code>npm ci<\/code>) rather than <code>npm install<\/code>. This checks your <code>package-lock.json<\/code> strictly to ensure the runner matches your exact local dependency tree.<\/td><\/tr><tr><td><code>run: npm run lint<\/code><\/td><td>Static validation<\/td><td>Executes our syntax validation script. If a file has a syntax error, this step exits with code <code>1<\/code>, halting the pipeline immediately.<\/td><\/tr><tr><td><code>run: npm test<\/code><\/td><td>Dynamic validation<\/td><td>Runs the unit tests. If any assertion fails, the test runner exits with code <code>1<\/code>, immediately marking the build as failed.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Execute and Validate the Pipeline<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">With the workflow file saved, commit and push it to trigger execution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git add .github\/workflows\/ci.yml\ngit commit -m \"ci: add GitHub Actions workflow configuration\"\ngit push origin main\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Inspecting the Run<\/h3>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Navigate to your repository on GitHub in your web browser.<\/li>\n\n\n\n<li>Click on the <strong>Actions<\/strong> tab along the top navigation bar.<\/li>\n\n\n\n<li>You will see an active or recently completed workflow labeled <strong>Continuous Integration<\/strong>.<\/li>\n\n\n\n<li>Click on the workflow run, then click on the <strong>validate-and-test<\/strong> job in the left-hand panel.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Plaintext<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\u2713 Set up job\n\u2713 Check out repository code\n\u2713 Set up Node.js runtime\n\u2713 Install dependencies\n\u2713 Validate code syntax (Lint)\n\u2713 Execute automated unit tests\n\u2713 Complete job\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each step can be expanded to view standard output (<code>stdout<\/code>) and standard error (<code>stderr<\/code>) generated directly by the underlying Linux runner.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Test Failure Handling (Simulating a Broken Build)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A pipeline that only passes tells you that your code works today. You must also prove that it successfully catches broken code before trusting it with your software delivery.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Introduce an Intentional Bug<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create a new feature branch:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git checkout -b break-the-calculator\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Open <code>src\/calculator.js<\/code> and modify the <code>add<\/code> function so that it introduces a logic bug:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">JavaScript<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function add(a, b) {\n  if (typeof a !== 'number' || typeof b !== 'number') {\n    throw new TypeError('Both arguments must be numbers');\n  }\n  return a - b; \/\/ BUG: Changed addition to subtraction\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. Push and Open a Pull Request<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Commit the regression and push the branch:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git add src\/calculator.js\ngit commit -m \"refactor: optimize arithmetic operations\"\ngit push origin break-the-calculator\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Navigate to GitHub and open a <strong>Pull Request<\/strong> comparing <code>break-the-calculator<\/code> into <code>main<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Observe the Failed Pipeline<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Within seconds, the <code>pull_request<\/code> event triggers the workflow. Because of our intentional logic bug, the assertion inside <code>test\/calculator.test.js<\/code> fails:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Plaintext<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\u2716 add() correctly sums two numbers (1.2ms)\n  AssertionError &#091;ERR_ASSERTION]: Expected values to be strictly equal:\n  -1 !== 5\n      at TestContext.&lt;anonymous&gt; (test\/calculator.test.js:6:10)\n\n\u2139 tests 3\n\u2139 suites 0\n\u2139 pass 2\n\u2139 fail 1\nError: Process completed with exit code 1.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub automatically flags the pull request with a red cross, alerting reviewers that the change contains an unhandled regression and should not be merged.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Practical Troubleshooting Guide<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When building pipelines from scratch, things rarely work on the first try. Use this reference matrix to quickly diagnose common failures:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><td><strong>Symptom<\/strong><\/td><td><strong>Probable Cause<\/strong><\/td><td><strong>Corrective Action<\/strong><\/td><\/tr><\/thead><tbody><tr><td><strong>Workflow does not trigger on push<\/strong><\/td><td>Incorrect directory structure or filename<\/td><td>Ensure the path is exactly <code>.github\/workflows\/&lt;name&gt;.yml<\/code>. Verify that hidden folders (<code>.github<\/code>) are tracked by Git.<\/td><\/tr><tr><td><strong>Invalid YAML syntax error<\/strong><\/td><td>Indentation or tab characters<\/td><td>YAML strictly forbids tab characters (<code>\\t<\/code>). Use 2 spaces for indentation. Validate formatting using an online linter or CLI tool like <code>yamllint<\/code>.<\/td><\/tr><tr><td><strong>Command not found (Exit code 127)<\/strong><\/td><td>Missing runtime setup step<\/td><td>If running <code>node<\/code>, <code>npm<\/code>, <code>python<\/code>, or <code>go<\/code>, confirm that the corresponding setup action (e.g., <code>actions\/setup-node<\/code>) executed <em>before<\/em> calling the tool.<\/td><\/tr><tr><td><strong><code>npm ci<\/code> fails on the runner<\/strong><\/td><td>Missing <code>package-lock.json<\/code><\/td><td><code>npm ci<\/code> requires an existing <code>package-lock.json<\/code> file in version control. Run <code>npm install<\/code> locally, commit the lockfile, and push.<\/td><\/tr><tr><td><strong>File or path not found<\/strong><\/td><td>Case-sensitivity mismatch<\/td><td>Ubuntu runners use case-sensitive filesystems. If your local machine runs macOS or Windows (case-insensitive), <code>require('.\/Calculator')<\/code> will work locally but crash in CI. Align all file casing.<\/td><\/tr><tr><td><strong>Permission Denied (HTTP 403)<\/strong><\/td><td>Insufficient default runner permissions<\/td><td>Check the <code>permissions:<\/code> block in your workflow. For basic CI that only reads source code, <code>contents: read<\/code> is sufficient.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Essential Security and Maintenance Best Practices<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once your first pipeline is running smoothly, apply these fundamental engineering practices to keep it secure and maintainable:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Pin Actions to Explicit Major Versions or SHAs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Avoid unversioned actions or floating <code>@latest<\/code> tags (e.g., <code>actions\/checkout@latest<\/code>). If an upstream dependency is compromised or introduces a breaking update, your pipeline will fail unexpectedly. Always pin actions to a specific major version tag (<code>@v4<\/code>) or an immutable commit SHA:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>uses: actions\/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. Never Hardcode Sensitive Credentials<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Never place API keys, private certificates, or database passwords in source code or YAML configuration files. Store them under <strong>Settings &gt; Secrets and variables &gt; Actions<\/strong> in your repository, and inject them into specific steps using environment variables:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">YAML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>- name: Run Integration Suite\n  env:\n    API_KEY: ${{ secrets.THIRD_PARTY_API_KEY }}\n  run: npm run test:integration\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. Keep Workflow Files Small and Purpose-Driven<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Avoid inlining long shell scripts directly inside YAML <code>run<\/code> steps. If a build task requires more than 5\u201310 lines of procedural shell commands, extract it into an executable script file (e.g., <code>.\/scripts\/build.sh<\/code>), test that script locally, and call the script from your pipeline step.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Next Steps for Maturing Your Delivery Pipeline<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A working build-and-test CI check provides a solid automation foundation. As your application evolves, consider adding the following stages in logical progression:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Plaintext<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Phase 1 (Current)        Phase 2 (Quality &amp; Security)      Phase 3 (Delivery)\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 \u2022 Checkout Code \u2502 \u2500\u2500\u2500\u25ba \u2502 \u2022 Static Analysis (ESLint)\u2502 \u2500\u2500\u25ba \u2502 \u2022 Build Container     \u2502\n\u2502 \u2022 Unit Tests    \u2502      \u2502 \u2022 Dependency Audit (npm)  \u2502     \u2502 \u2022 Push to Registry    \u2502\n\u2502 \u2022 Syntax Check  \u2502      \u2502 \u2022 Code Coverage Reports   \u2502     \u2502 \u2022 Deploy to Staging   \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>Static Analysis &amp; Linting Tools:<\/strong> Incorporate mature linters (such as ESLint or Prettier) to enforce consistent code formatting and flag anti-patterns across the team.<\/li>\n\n\n\n<li><strong>Automated Vulnerability Scanning:<\/strong> Add an <code>npm audit<\/code> step or integrate tools like Trivy or Snyk to alert you when third-party packages contain published CVEs.<\/li>\n\n\n\n<li><strong>Artifact Archiving:<\/strong> Use <code>actions\/upload-artifact@v4<\/code> to store compiled distributions, coverage reports, or logs so they can be downloaded and reviewed after the run finishes.<\/li>\n\n\n\n<li><strong>Environment Deployment:<\/strong> Add a downstream deployment job that triggers only when commits merge into your production branch, deploying your code automatically to staging or production environments.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently Asked Questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Which tool is best for building a first pipeline?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub Actions and GitLab CI\/CD are the best starting points for beginners because their configuration files live directly inside your repository, and they provide managed runner infrastructure out of the box. Traditional on-premise engines like Jenkins require significant server administration, network configuration, and security patching before you can run your first job.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I build a CI pipeline without deploying to a live server?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. In fact, you <em>should<\/em> start without deployment. A continuous integration pipeline&#8217;s primary goal is verification: confirming that changes compile, adhere to coding standards, and pass automated tests. Deployment is a secondary phase (Continuous Delivery) that should only occur after your CI checks pass consistently.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why did my pipeline pass locally but fail in CI?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This discrepancy usually comes down to environment drift:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>Uncommitted files:<\/strong> You have local files or configs that are listed in <code>.gitignore<\/code> or were never added to Git.<\/li>\n\n\n\n<li><strong>Global dependencies:<\/strong> You have software installed globally on your workstation that the fresh CI runner does not have.<\/li>\n\n\n\n<li><strong>OS-level differences:<\/strong> Your workstation OS (macOS\/Windows) handles path separators and filename casing differently than the Linux host running inside CI.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">How do automated tests stop broken code from reaching production?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pipeline stages run sequentially. When an assertion in an automated test fails, the test process exits with an exit code greater than 0. The pipeline runner registers this status, immediately terminates downstream steps (such as deployment or packaging), marks the run as failed, and reports this failure to your branch protection rules to block merging.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Building an automated pipeline does not require complex cloud architecture or enterprise toolchains. By defining clear triggers, provisioning a managed runner, and codifying your build, lint, and test steps into a declarative configuration file, you establish a deterministic safety net for your project. The pipeline you built today checks out code, sets up runtimes, validates syntax, and prevents broken logic from merging into your codebase. Treat your pipeline configuration as first-class code: keep it versioned, test it periodically by introducing intentional regressions, and gradually expand its capabilities as your system grows.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Every production issue that stems from an untested code change, a forgotten build step, or a missed lint error shares a common origin: manual execution. When steps rely on an&hellip;<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1767","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/posts\/1767","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/comments?post=1767"}],"version-history":[{"count":1,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/posts\/1767\/revisions"}],"predecessor-version":[{"id":1769,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/posts\/1767\/revisions\/1769"}],"wp:attachment":[{"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/media?parent=1767"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/categories?post=1767"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devopsschool.org\/blog\/wp-json\/wp\/v2\/tags?post=1767"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}