CI/CD: Automating Tests, Builds and Deployments
Exit

Lesson 1 of 10 · Principles and GitHub Actions

Continuous integration and deployment: principles, pipeline, environments

40 min Node.js 22 (Debian, git, npm, python3)

Objective

Understand why teams automate their tests and their production releases, what the acronyms CI and CD cover exactly, how a pipeline is structured, and what environments are for. You will then write your first pipeline… in plain shell, to see clearly what a tool such as GitHub Actions or GitLab CI automates.

Key points

  • Continuous integration (CI): every pushed change is built and tested automatically, several times a day, to detect problems immediately.
  • Continuous delivery: every version that passes CI is ready to go to production; the production release remains a human click.
  • Continuous deployment: every version that passes all the checks goes to production automatically.
  • A pipeline chains steps (lint, tests, build, deployment); the first one that fails stops everything.
  • Environments (development, staging, production) let you validate a version before it reaches customers.
  • Core principle: you build only once and deploy the same artifact everywhere.

1. The problem: "it works on my machine"

At the agency Atlas Web (Casablanca), before automation, a production release looked like this:

  • on Friday evening, Youssef merges three weeks of work by four people;
  • nobody has run all the tests: some have been failing for ten days;
  • he builds the site on his workstation (with his version of Node.js), then copies the files to the server over FTP;
  • a client's website goes down on Saturday; nobody knows which change caused it.

Continuous integration and deployment solve these problems with three ideas:

Idea In practice
Integrate often short branches merged every day: conflicts stay small
Check everything automatically a neutral machine runs lint, tests and build on every push
Deploy the way you test deployment is a versioned script, identical every time, and reversible

2. CI, continuous delivery, continuous deployment

                         Continuous integration (CI)
            ┌──────────────────────────────────────────────┐
  push  →   │ fetch the code → lint → tests → build        │ → artifact
            └──────────────────────────────────────────────┘
                                                              │
     Continuous delivery: automatic deployment to staging,    │
     to production on a human decision (button)  ─────────────┤
                                                              │
     Continuous deployment: everything is automatic up to     │
     production, if all checks are green  ────────────────────┘
Continuous integration Continuous delivery Continuous deployment
Build and tests on every push yes yes yes
Artifact ready to deploy sometimes always always
Production release manual, outside the pipeline one click in the pipeline automatic
Prerequisites automated tests + scripted deployments + strong confidence in tests and monitoring

In practice, people often say "CI/CD" for the whole thing. An agency like Atlas Web typically does continuous delivery: every merge into main goes automatically to staging, and the project manager triggers the production release after the client's approval.

3. Anatomy of a pipeline

A pipeline is split into stages that contain jobs. Jobs in the same stage can run in parallel; stages run in order.

  "check" stage         "test" stage            "build" stage           "deploy" stage
  ┌──────────┐            ┌──────────────┐        ┌──────────────┐        ┌──────────────┐
  │ lint     │            │ tests Node 20│        │ Docker image │        │ staging      │
  │ format   │   ───→     │ tests Node 22│  ───→  │ archive      │  ───→  │ production   │
  └──────────┘            └──────────────┘        └──────────────┘        │ (manual)     │
                                                                          └──────────────┘
Step Role Examples
Lint / format check style and obvious errors, in a few seconds ESLint, Prettier, shellcheck, php -l
Tests prove that the code does what it should unit, integration and end-to-end tests
Security find vulnerable dependencies and secrets npm audit, code analysis, secret detection
Build produce the artifact to deploy archive, Docker image, binary, mobile app
Deployment install the artifact in an environment SSH to a VPS, Kubernetes, hosting provider

The pipeline runs on a runner (or executor): a machine, often a fresh virtual machine or container, provided by GitHub or GitLab, or installed on your own servers. Because it starts from scratch every time, "it works on my machine" disappears.

The good properties of a pipeline

  • Fast: less than 10 minutes for the main feedback; beyond that, developers stop waiting for the result.
  • Reliable: a test that fails at random (flaky) destroys trust; you fix it or remove it.
  • Versioned: the pipeline is a file in the repository (.github/workflows/*.yml, .gitlab-ci.yml), reviewed in pull requests like the code.
  • Red = priority: when main is broken, the team fixes it before moving on.

4. Environments

Environment Role Who uses it Data
Development (local) write and try out the developer fictitious
Review (review app) see a pull request online reviewers, designers fictitious
Staging validate a version under conditions close to production project manager, client anonymized copy or test data set
Production serve real customers everyone real

Rules that prevent disasters:

  • The same artifact moves from staging to production: you do not rebuild between the two, otherwise what you validated is not what you deploy.
  • Only the configuration changes from one environment to another (database address, API keys), through variables and secrets specific to each environment.
  • Production is protected: deployment restricted to certain branches, mandatory approval, secrets that other jobs cannot access.
  • You can always roll back to the previous version (lesson 3.2).

5. The tool landscape

Tool Where the pipeline lives Specific feature
GitHub Actions .github/workflows/*.yml built into GitHub, huge catalog of reusable actions
GitLab CI/CD .gitlab-ci.yml built into GitLab, runners you can install anywhere, widely used in companies
Jenkins Jenkinsfile historical, self-hosted, extensible with plugins
Others Azure Pipelines, Bitbucket Pipelines, CircleCI, Drone/Woodpecker… same logic: a YAML file, steps, runners

Levels 1 and 2 of this course cover GitHub Actions, then GitLab CI. Once you understand the principles, moving from one to the other is mostly a matter of vocabulary.

6. A pipeline by hand, to understand

Before handing the work to a platform, let's write the same sequence in shell. This is exactly what a runner will do: commands, in order, stopping at the first error.

#!/bin/sh
# scripts/pipeline.sh — local pipeline for the Atlas storefront
set -e                      # stop at the first command that fails

echo "== lint"
npm run lint

echo "== tests"
npm test

echo "== build"
mkdir -p dist
tar -czf dist/vitrine.tar.gz package.json src
echo "Artefact prêt : dist/vitrine.tar.gz"
chmod +x scripts/pipeline.sh
./scripts/pipeline.sh
echo "code : $?"            # 0 = green pipeline; anything else = red

set -e is the key: without it, the script would continue after a failed test and produce a broken artifact. CI platforms apply the same rule: a command that returns a non-zero code makes the job fail.

You can even connect this pipeline to Git with a pre-push hook (Git course, lesson 3.2): the push is rejected if the pipeline fails. This is useful, but it stays on your workstation and can be bypassed with --no-verify. Real CI runs on the server, for everyone, on every push.

7. Measuring: the four metrics

To know whether your delivery chain is improving, teams often track four metrics (called "DORA" metrics):

Metric Question
Deployment frequency how many production releases per week?
Lead time for changes how long between a commit and its release?
Change failure rate what share of deployments causes an incident?
Time to restore how long to get back to normal service after an incident?

Good CI/CD raises the first two and lowers the last two at the same time: you deliver more often and more safely, because each delivery is small.

8. Where to start in an existing team

There is no need to automate everything at once. Here is the plan Atlas Web followed, over about a month:

Week Goal Visible result
1 lint + existing tests run on every pull request a green check or a red cross on every PR
2 main protected: green CI and one review required no more direct pushes, no more "it worked on my machine"
3 artifact built by CI, automatic deployment to staging the client sees every merge online the same day
4 one-click production deployment, with a documented rollback no more Friday-evening FTP copies

Only then come optimization (cache, parallelism), advanced security (OIDC, signatures, level 4) and, if the team wants it, full continuous deployment.

Two tips to make this transition succeed:

  • Measure before and after: the average duration of a production release, the number of incidents per month. These figures convince better than a speech.
  • Keep the pipeline simple and readable: a YAML file that the whole team understands is better than an overcomplicated machine that only one person can repair.

Common pitfalls

  • Automating chaos: without automated tests, a pipeline only deploys bugs faster. Start with a few tests on the critical functions (price calculation, payment).
  • Rebuilding for production what was validated in staging: it is no longer the same artifact.
  • A 40-minute pipeline: developers push less often and work around CI. Parallelize and use caching (lesson 1.3).
  • Ignoring a red pipeline "because this test always fails": fix it or delete it, but do not get used to red.

Lab

In the Node.js project /root/vitrine (already in Git, with a test and a lint), you build a local pipeline:

  1. scripts/pipeline.sh: lint then tests, stopping at the first error;
  2. a build step that produces the artifact dist/vitrine.tar.gz, only if everything is green;
  3. a commit of the script, without the artifact;
  4. a pre-push hook that runs the pipeline before every push.

The checks deliberately break a test in a copy of the project to make sure that your pipeline really turns red.

Quiz

  1. What is the difference between continuous delivery and continuous deployment?
  2. Why does a pipeline stop at the first failed step?
  3. What is a runner, and why does it start from a fresh machine?
  4. Why must you deploy to production the same artifact as the one validated in staging?
  5. What does set -e do in a pipeline script?
Answers
  1. In continuous delivery, every validated version is ready and the production release is triggered by a human decision; in continuous deployment, it goes to production automatically if all the checks pass.
  2. So that you never build or deploy code that you know is defective, and to give quick feedback on the cause of the failure.
  3. It is the machine (or container) that runs the pipeline jobs. It starts from a clean state so that the result does not depend on whatever is left on a particular workstation.
  4. Because rebuilding can produce a different result (updated dependencies, another environment): what you would deploy would no longer be what you tested.
  5. It stops the script as soon as a command returns a non-zero code, and the script itself then ends in failure.

Reference sources: GitHub Docs (CC BY 4.0) and GitLab Docs (CC BY-SA 4.0). Original MasterLinux text.

Practical exercises

To do on Node.js 22 (Debian, git, npm, python3)

The online terminal is coming soon. Meanwhile, do these tasks on your own Linux machine, a virtual machine or WSL.

  1. In /root/vitrine, write the executable script scripts/pipeline.sh that first runs npm run lint, then npm test, and that STOPS with a non-zero exit code as soon as a step fails.
    Show a hintFirst line #!/bin/sh, then set -e (the script stops at the first failing command), then npm run lint and npm test. chmod +x.
    Show the solution
    mkdir -p /root/vitrine/scripts && printf '%s\n' '#!/bin/sh' 'set -e' 'echo "== lint"' 'npm run lint' 'echo "== tests"' 'npm test' > /root/vitrine/scripts/pipeline.sh && chmod +x /root/vitrine/scripts/pipeline.sh

    Check : cd /root/vitrine && test -x scripts/pipeline.sh && ./scripts/pipeline.sh >/dev/null 2>&1 && d=$(mktemp -d) && cp -r /root/vitrine/. "$d" && echo 'test("casse", () => { throw new Error("casse"); });' >> "$d/test/prix.test.js" && ! (cd "$d" && ./scripts/pipeline.sh >/dev/null 2>&1)

  2. Add a build step at the end of the pipeline: it produces the archive dist/vitrine.tar.gz containing package.json and the src/ folder. The archive must be produced ONLY if the lint and the tests succeeded.
    Show a hintAfter npm test: mkdir -p dist && tar -czf dist/vitrine.tar.gz package.json src. Thanks to set -e, this line is never reached if a previous step fails.
    Show the solution
    printf '%s\n' '#!/bin/sh' 'set -e' 'echo "== lint"' 'npm run lint' 'echo "== tests"' 'npm test' 'echo "== build"' 'mkdir -p dist' 'tar -czf dist/vitrine.tar.gz package.json src' > /root/vitrine/scripts/pipeline.sh && chmod +x /root/vitrine/scripts/pipeline.sh

    Check : cd /root/vitrine && rm -rf dist && ./scripts/pipeline.sh >/dev/null 2>&1 && tar -tzf dist/vitrine.tar.gz | grep -q "src/prix.js" && d=$(mktemp -d) && cp -r /root/vitrine/. "$d" && rm -rf "$d/dist" && echo 'test("casse", () => { throw new Error("casse"); });' >> "$d/test/prix.test.js" && ! (cd "$d" && ./scripts/pipeline.sh >/dev/null 2>&1) && ! test -f "$d/dist/vitrine.tar.gz"

  3. Commit scripts/pipeline.sh. The dist/ archive must not be tracked by Git, and the repository must be clean (git status has nothing to report).
    Show a hintdist/ is already in .gitignore: git add scripts/pipeline.sh then git commit -m "ci: ajoute le pipeline local"
    Show the solution
    cd /root/vitrine && git add scripts/pipeline.sh && git commit -qm "ci: ajoute le pipeline local"

    Check : cd /root/vitrine && git ls-files | grep -qx scripts/pipeline.sh && ! git ls-files | grep -q "^dist/" && test -z "$(git status --porcelain)"

  4. Make this pipeline run automatically before every git push: create the executable hook .git/hooks/pre-push that calls scripts/pipeline.sh (a push will be rejected if the pipeline fails).
    Show a hintprintf '%s\n' '#!/bin/sh' 'exec ./scripts/pipeline.sh' > .git/hooks/pre-push && chmod +x .git/hooks/pre-push (the hook runs from the root of the repository)
    Show the solution
    cd /root/vitrine && printf '%s\n' '#!/bin/sh' '# Le push est refuse si le pipeline echoue' 'exec ./scripts/pipeline.sh' > .git/hooks/pre-push && chmod +x .git/hooks/pre-push

    Check : cd /root/vitrine && test -x .git/hooks/pre-push && grep -q "pipeline.sh" .git/hooks/pre-push && d=$(mktemp -d) && git init -q --bare "$d/r.git" && git push -q "$d/r.git" HEAD:refs/heads/main >/dev/null 2>&1

Official references consulted: GitHub Docs — Intégration continue — https://docs.github.com/fr/actions/get-started/continuous-integration ; GitHub Docs — Déploiement continu — https://docs.github.com/fr/actions/get-started/continuous-deployment ; GitLab Docs — Get started with GitLab CI/CD — https://docs.gitlab.com/ci/ ; GitLab Docs — CI/CD pipelines — https://docs.gitlab.com/ci/pipelines/ ; GitLab Docs — Environments — https://docs.gitlab.com/ci/environments/. Original MasterLinux text.