The Pain of “Manual Deploys” and the Phrase “But, it works on my machine?”
Every developer is familiar with this scenario: you finish coding a feature you’re proud of, test it thoroughly on your machine, and then type git push with full confidence. A few minutes later, “ping, ping”—a series of messages from the QA team or your boss: “Hey, the app crashed,” “Feature ABC isn’t working,”… The feeling at that moment is truly hard to describe.
I vividly remember the first web app project I joined. The team had 5 devs, constantly merging code into the develop branch. Every weekend release was a “hold your breath” moment. The person assigned to deploy had to log into the server, git pull, run a series of build commands, and restart the service. Just one wrong environment variable could bring the whole system down. The deployment process took a stressful 30-45 minutes, and more importantly, I couldn’t sleep well.
That’s the price of lacking automation: it wastes time, drains morale, and is rife with risks from simple human error.
Why Suffer? Analyzing the Root of the Problem
Looking deeper, these “pains” usually stem from three main causes:
- Fragmented, inconsistent processes: Each developer has different habits. Some run tests before pushing, some don’t. One uses
npm, another prefersyarn, leading to unsynchronized lock files (package-lock.jsonvsyarn.lock). Without a common standard, conflicts and errors are inevitable. - Lack of automated testing: Running tests depends on each individual’s discipline. When a project is rushed, this step is often skipped. The result is that buggy code gets merged directly into the main branch. Detecting and fixing bugs at a later stage is much more costly.
- Environment drift: The phrase “But it works on my machine” is a direct consequence of this. A dev’s local environment (e.g., macOS with Node v18) can be vastly different from the production environment (e.g., Ubuntu with Node v16). Differences in library versions, environment variables, or operating system configurations are the source of countless bizarre bugs.
Common “Tricks” (and Why They’re Not Good Enough)
To deal with this situation, teams often adopt a few methods:
Method 1: Appoint a “Deployment Warrior”
A single person (usually a team lead or DevOps engineer) is responsible for all deployments. This ensures a bit more consistency but turns that person into a number one “bus factor.” Everything gets bottlenecked if they are busy or on leave. The risk of human error remains.
Method 2: Write a custom deploy script (e.g., `deploy.sh`)
This is a step up. We create a script file containing the necessary commands to deploy. When needed, you just run that file.
#!/bin/bash
echo "🚀 Starting deployment..."
# Pull the new version
git pull origin main
# Install dependencies
npm install
# Build the project
npm run build
# Restart server with pm2
pm2 restart my-app
echo "✅ Deployment finished!"
It’s an improvement, but it still has weaknesses. Who runs this script and when? How do you securely manage API keys and passwords? And how does the whole team view the deployment history?
The Ultimate Solution: CI/CD with GitHub Actions
This is where CI/CD shines. It thoroughly solves the above problems by automating the entire process, creating a consistent and secure workflow.
What is CI/CD? A Quick Primer
- CI (Continuous Integration): Imagine that every time you push code, a bot automatically fetches it, builds it, and runs all the tests. The goal is to detect errors immediately, ensuring new code doesn’t break what’s already working.
- CD (Continuous Deployment/Delivery): After CI reports success (the code has passed all tests), the bot automatically deploys the code to a specified environment. This could be a Staging environment for the QA team to review, or Production for end-users.
And GitHub Actions is GitHub’s own “homegrown” CI/CD tool—powerful, free for public projects, and integrated right into the familiar interface you already use.
Hands-On: Building Your First CI/CD Workflow for a Node.js Project
Enough theory, let’s get to work. I’ll guide you through creating a simple CI workflow: every time new code is pushed to the `main` branch, GitHub Actions will automatically set up the environment, install dependencies, and run unit tests.
Step 1: Create the workflow directory and file
In the root directory of your project, create the following folder structure: .github/workflows/. Inside the `workflows` directory, create a file named ci.yml.
Step 2: Write the automation script using YAML
Open the ci.yml file and paste the following code into it:
name: Node.js CI
# Trigger: Run on push or pull request to the "main" branch
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build_and_test:
# Run on the latest version of an Ubuntu virtual machine provided by GitHub
runs-on: ubuntu-latest
strategy:
matrix:
# Powerful feature: Run tests in parallel across multiple Node.js versions
node-version: [16.x, 18.x, 20.x]
steps:
# Step 1: Check out the repository code to the runner
- name: Checkout repository
uses: actions/checkout@v4
# Step 2: Set up the corresponding Node.js version from the matrix
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
# Pro-tip: Enable caching to speed up dependency installation in subsequent runs
cache: 'npm'
# Step 3: Install dependencies (using npm ci for safety and speed)
- name: Install dependencies
run: npm ci
# Step 4: Run the build command (if present in package.json)
- name: Build project
run: npm run build --if-present
# Step 5: Run tests
- name: Run tests
run: npm test
Step 3: Analyzing the script
name: The name of the workflow, which will be displayed on the Actions tab in GitHub.on: The trigger. The workflow will run on apushor when apull_requestis created/updated for themainbranch.jobs: Contains the tasks to be performed. A workflow can have one or more jobs.build_and_test: The name of our job.runs-on: Specifies the operating system for the virtual machine running the job.<a href="https://itfromzero.com/en/development-vi-en/complete-guide-to-installing-and-configuring-apache2-on-ubuntu-22-04.html">ubuntu-latest</a>is the most popular choice.strategy.matrix: A powerful tool for testing across multiple environments. This job will be duplicated and run in parallel on three Node.js versions: 16.x, 18.x, and 20.x, ensuring your code has wide compatibility.steps: The sequential steps that the job will execute.uses: Leverages ready-made ‘actions’ from the community to avoid ‘reinventing the wheel’.actions/checkout@v4andactions/setup-node@v4are two of the most popular actions, maintained by GitHub itself.run: Executes a command-line command on the virtual machine.
Now, every time you push code, go to the “Actions” tab in your GitHub repository. You will see your workflow running. If there’s an error at any step (e.g., a test fails), the workflow will turn red, and you will receive an email notification immediately.
Tips and Best Practices from “Painful” Experience
- Keep API keys and tokens absolutely secure: Never hardcode sensitive information. This is a serious security vulnerability. Use GitHub Secrets instead. Go to
Settings > Secrets and variables > Actions, create a new secret (e.g.,DATABASE_URL). In your workflow, you can access it securely viaenv: DATABASE_URL: ${{ secrets.DATABASE_URL }}`. This data is encrypted and never exposed in the logs. - Use
npm ciinstead ofnpm install:npm ciinstalls the exact versions from thepackage-lock.jsonfile, ensuring the CI environment is identical to the dev’s environment. It’s also faster and safer for automated processes. - Leverage caching to speed things up: As in the example, using
with: cache: 'npm'helps save downloaded libraries (thenode_modulesdirectory). On subsequent runs, if thepackage-lock.jsonfile hasn’t changed, GitHub Actions will restore the cache instead of re-downloading everything. For large projects, this step can save anywhere from 30 seconds to several minutes. - Separate CI and CD: Use separate
jobs for each stage (e.g.,test,build,deploy). Use theneeds: [test, build]keyword to ensure thedeployjob is only triggered after thetestandbuildjobs have completed successfully 100%.
Returning to the story of the project with 5 developers, after implementing CI/CD, the deployment time dropped from a stressful 30 minutes to about 5 minutes of complete automation. More importantly, the entire team became much more confident, no longer afraid that pushing code would bring down the system. With just a simple YAML file, you not only save time but also build a solid DevOps culture: automated, safe, and transparent.

