Build the mental model
CI/CD is two ideas that grew up together, and it pays to separate them. Continuous Integration is the practice of every developer merging their work into one shared branch frequently - at least daily - and having every one of those merges automatically verified by a build and a test suite. The word integration is doing real work there. Before CI, teams worked on long-lived branches for weeks and then spent a miserable week discovering that three people had rewritten the same module in mutually incompatible ways. That week had a name: integration hell. CI does not remove conflicts. It makes them small by making them frequent, and it makes them visible by checking automatically instead of hoping somebody remembers to run the tests before pushing.
CD is where people get confused, because the letters stand for two different things. Continuous Delivery means every commit that passes CI produces an artifact that could be released at any moment, and a human decides when to press the button. Continuous Deployment removes the button entirely: a green build goes to production by itself. Delivery is a technical property of your pipeline; Deployment is an organisational decision about how much risk you are willing to automate. Most teams should reach Delivery first, and plenty of them should never go further.
The value underneath all of it is feedback-loop length. A bug found ninety seconds after you write it is a small correction while the code is still in your head. The same bug found three weeks later in staging is an archaeological dig through other people's commits. Everything CI/CD asks of you - small merges, fast tests, automated checks - exists to shorten that gap.
COMMIT TO PRODUCTION: THE CI/CD PIPELINE
----------------------------------------
developer shared repository
+-----------+ push +--------------------+
| feature | ----------------> | pull request |
+-----------+ +--------------------+
|
v
+----------------------+
| CI: build + test |
| lint / unit / e2e |
+----------------------+
| |
red X green check
| |
v v
back to dev merge to main
|
v
+----------------------+
| CD: package + ship |
+----------------------+
| | |
v v v
staging canary production
DELIVERY = a human approves the last arrow
DEPLOYMENT = the last arrow happens on its ownConnect it to a real scenario
Starting with CI takes no grand architectural decision. Create one file at .github/workflows/ci.yml in your repository and commit it. The workflow below runs on every push to main and on every pull request. Its first step, actions/checkout@v4, copies your code onto the runner - without it the runner is a bare machine with none of your files on it, which is the single most common beginner surprise. The second step installs Node 20 and switches on npm caching so later runs install faster. Then npm ci installs dependencies exactly as pinned in the lockfile - not npm install, which is allowed to update the lockfile and quietly make your CI run differ from a teammate's. Finally npm test runs the suite.
The mechanism that makes all of this work is the exit code. If any step exits non-zero, the step fails, the job fails, and GitHub marks the commit with a red X. That red X is only information until you make it enforcement: go to Settings, Branches, add a branch protection rule for main, and turn on "Require status checks to pass before merging". Until you do that, everybody can see the failure and merge anyway. A CI pipeline nobody is required to obey is a very expensive decoration.
Try the working example
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Install dependencies from the lockfile
run: npm ci
- name: Run the test suite
run: npm test
A push to main, or opening or updating a pull request, triggers this workflow. There is a single job named build, which runs on a fresh ubuntu-latest runner and executes its four steps in order: check out the repository, install Node 20, install dependencies with npm ci, run npm test. If npm test exits non-zero the job stops at that step and the commit and pull request are marked with a red X; if every step exits zero the run is marked with a green check. Note that the red X blocks nothing on its own - without a branch protection rule requiring this check, the pull request can still be merged.5-minute try-it
Add the workflow above to one of your own repositories as .github/workflows/ci.yml. Then deliberately break one test, push it on a new branch, and open a pull request. In the Actions tab, note which step the job stopped at and what the pull request page shows. Now go to Settings, Branches, add a protection rule for main with "Require status checks to pass before merging" enabled, and look at the merge button again. Write down, in one sentence each, what Continuous Delivery and Continuous Deployment would mean for this specific repository.
One important caution
Putting the file anywhere other than .github/workflows/ - for example workflows/ci.yml or the repository root. GitHub does not warn you; the workflow simply never runs.
Treating a green check as the finish line without branch protection. If nobody is required to wait for the check, red builds get merged and the pipeline becomes decoration.
GitHub Docs - Understanding GitHub Actions — CI/CD with GitHub Actions