Your First CI/CD Pipeline: A Guide for Small IT Teams
A two-person IT team pushes a config change straight to the production branch on a Friday afternoon, the way they've pushed the last thirty changes — no test run, no second set of eyes, just a commit and a deploy. It works right up until the change that breaks the login flow ships at 4:45pm with nobody watching, and the first person to notice is a customer. A CI/CD pipeline is the mechanism built to catch exactly that failure before it reaches production: a defined, automatic sequence that builds and tests every change, and only ships the ones that pass.
This guide covers what continuous integration and continuous deployment actually mean as separate steps, walks through a minimal working pipeline on GitHub Actions — trigger on push, run tests, deploy only when they pass — and sets it side by side with GitLab CI's equivalent setup, so a small team can pick a platform and have a working gate running in an afternoon instead of piecing the concepts together from vendor marketing pages.
What CI and CD Actually Mean
Continuous integration is the practice of automatically building and testing every change to a codebase, typically the moment it's pushed, so the team learns whether a change is broken within minutes rather than at the next manual test session — or worse, after it's already live. Continuous deployment is a distinct second step layered on top of that: once the automated build and test pass, the same pipeline pushes the change to production without a person manually running the release. A team can adopt the first without the second — running automated tests on every push but still deploying by hand — which is a reasonable place to start before trusting the pipeline with production pushes.
GitHub describes GitHub Actions as "a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline," and its own documentation breaks the platform into a small set of parts worth knowing by name. A workflow is a configurable automated process, defined in a YAML file, that runs when triggered by an event in the repository — a push, a pull request, a schedule. A workflow contains one or more jobs, each of which runs inside its own virtual machine or container called a runner. Each job runs through one or more steps, and a step either runs a shell command directly or calls a reusable action — a packaged piece of automation someone else already wrote, so a team isn't hand-rolling the logic for checking out code or setting up a language toolchain from scratch. GitLab's model uses almost the same shape under different names: a .gitlab-ci.yml file defines jobs grouped into stages, and a runner — GitLab's term for the same kind of execution agent — carries out each job's script.
A Minimal Working Pipeline on GitHub Actions
GitHub's own quickstart guide is built around a single rule that trips up a lot of first-time setups: for GitHub to discover a workflow at all, the YAML file has to live in a directory literally named .github/workflows at the root of the repository, with a .yml or .yaml extension. Get that path wrong and the workflow simply never runs — there's no error, because GitHub never finds it to try.
A minimal pipeline that tests on every push and only deploys when the tests pass can be as short as this:
1name: Test and Deploy
2on: [push]
3jobs:
4 test-and-deploy:
5 runs-on: ubuntu-latest
6 steps:
7 - uses: actions/checkout@v6
8 - run: npm test
9 - run: ./deploy.sh
The on: [push] line is the trigger — this workflow fires on every push to the repository, the same event mechanism GitHub's quickstart guide uses in its own example workflow. The actions/checkout@v6 step is a reusable action, referenced by exact version, that pulls the repository's code onto the runner before anything else can act on it — without it, the runner has nothing to test. The two run: lines after it are plain shell commands executed on that runner in order.
That ordering is what actually delivers "deploy on green" without needing anything more elaborate. GitHub's documentation is explicit that "steps are executed in order and are dependent on each other" within a job — so if the npm test step exits with a failure, the ./deploy.sh step immediately after it never runs. The gate isn't a separate feature to configure; it's a property of steps running sequentially in the same job. A team that wants the test and deploy logic to run as fully separate jobs — for example, to deploy from a different runner or only on a specific branch — can split them, but for a first pipeline, one job with sequential steps is enough to get real protection running today.
What GitLab CI Looks Like for the Same Pattern
GitLab's version of the same idea lives in a .gitlab-ci.yml file at the root of the repository instead of a workflows directory, and organizes jobs into named stages rather than relying purely on step order inside one job. GitLab's own quick-start tutorial uses an example with a build stage, two jobs in a test stage, and a deploy-prod job in a deploy stage — each job carries a stage: key naming which phase it belongs to and a script: key with the shell commands to run. Because "stage describes the sequential execution of jobs," the deploy stage doesn't start until every job in the build and test stages ahead of it has finished, which is the same trigger-test-deploy shape as the GitHub example, just expressed as stage order instead of step order within one job.
One practical difference worth knowing before choosing a platform: GitLab's tutorial notes that jobs in the same stage run in parallel automatically, as long as more than one runner is available, so a team that splits its test suite into two jobs in the same test stage gets them running side by side without any extra configuration. GitLab also documents a needs keyword specifically for running jobs out of stage order when a team wants more control over sequencing than the default stage-by-stage flow provides — useful once a pipeline grows past the two-or-three-job stage, but not something a first pipeline needs.
Getting a runner in place is close to a non-issue for a team on either platform's hosted service. GitLab's tutorial states plainly that a project on GitLab.com can skip runner setup entirely, because "GitLab.com provides instance runners for you." Teams self-hosting GitLab, or without access to a shared runner, install GitLab Runner locally, register it against the project, and choose the shell executor — at which point jobs execute on that local machine instead of a hosted one. GitHub's model is the direct equivalent: GitHub provides Linux, Windows, and macOS virtual machines to run workflows out of the box, with self-hosted runners as the option for teams that need their own hardware or a specific environment GitHub doesn't offer.
Choosing a Platform and Getting the Gate Running Today
For most small teams, the deciding factor isn't a feature gap between the two platforms — it's where the code already lives. A team already hosting its repository on GitHub gets Actions with no separate service to sign up for; a team on GitLab gets the same thing from GitLab CI. Both platforms meter the compute time a workflow consumes against an included allowance tied to the account's specific plan, rather than charging a flat setup fee — but the exact size of that allowance, and how usage on private repositories is counted against it, is exactly the kind of detail that's worth checking directly in the account's own billing settings rather than assuming from a number seen somewhere else, since plan terms are the sort of thing platforms revise over time. The practical move for a new pipeline isn't to guess at a quota — it's to turn the workflow on, watch a few runs, and check the account's usage dashboard before scaling the test suite up significantly.
The case for building this even as a two-person team comes back to the failure mode in the opening scenario: manual testing is the step that gets skipped under time pressure, and time pressure is exactly when a mistake is most likely to slip through. A pipeline doesn't get tired on a Friday afternoon and doesn't decide a change is probably fine without checking. It also creates a single, visible signal — pipeline green or pipeline red — that both people on a small team can trust without re-running each other's tests by hand or asking "did you check this before you pushed it." That's a small amount of one-time YAML for a check that otherwise depends entirely on nobody being in a hurry, which is not a bet worth making with production.
Key Takeaways
- Continuous integration automatically builds and tests every push; continuous deployment is the separate step of shipping a change to production once those tests pass — a team can run the first without the second.
- A GitHub Actions workflow file must live at
.github/workflows/*.yml; sequential steps within one job are enough to gate a deploy, since a failed step stops the steps after it from running. - GitLab CI uses a
.gitlab-ci.ymlfile with named stages instead of a workflows directory; stages run in sequence by default, and jobs within a stage run in parallel when a runner is available. - Both platforms meter compute time against a plan-based allowance rather than a flat fee — check the account's own billing dashboard for the current number rather than assuming one.
- Neither platform requires standing up infrastructure to get started: GitHub and GitLab.com both provide hosted runners by default, so a first pipeline is a YAML file away, not an infrastructure project.
References
- Quickstart for GitHub Actions — GitHub's own walkthrough for creating a first workflow file and viewing its run results.
- Understanding GitHub Actions — official definitions of workflows, jobs, steps, actions, and runners.
- Tutorial: Create and run your first GitLab CI/CD pipeline — GitLab's quick-start guide, including a working
.gitlab-ci.ymlexample with stages and runners.