Sachith Dassanayake Software Engineering GitLab CI pipelines from scratch — Real‑World Case Study — Practical Guide (Apr 17, 2026)

GitLab CI pipelines from scratch — Real‑World Case Study — Practical Guide (Apr 17, 2026)

GitLab CI pipelines from scratch — Real‑World Case Study — Practical Guide (Apr 17, 2026)

GitLab CI pipelines from scratch — Real‑World Case Study

GitLab CI pipelines from scratch — Real‑World Case Study

Level: Intermediate

As of 17 April 2026, GitLab version 16.0 and later.

Introduction

Continuous Integration (CI) pipelines are fundamental to modern software development. GitLab CI/CD provides an integrated, flexible system for automating build, test, and deployment workflows directly within your Git repository. This article walks through creating a robust GitLab CI pipeline from scratch based on a real-world scenario, targeting intermediate developers familiar with basic Git and CI concepts.

We will focus on GitLab 16.x stable features, avoiding experimental flags or preview functionality.

Prerequisites

  • Access to a GitLab instance version 16.0 or newer (Cloud or self-managed)
  • A project repository with basic code (for example, a multi-language microservice app)
  • Familiarity with GitLab’s UI and Git workflows
  • Runner(s) registered and configured (shared runners are sufficient for initial steps)
  • Basic knowledge of Docker (optional but highly recommended)
  • Working knowledge of YAML syntax (for .gitlab-ci.yml files)

Hands-on Steps

Step 1: Define your pipeline stages

Start by outlining the phases your pipeline will have — typically:

  • build — Compile or create artefacts
  • test — Execute unit and integration tests
  • package — Prepare deliverables
  • deploy — Deploy to an environment (staging, production)

Minimal .gitlab-ci.yml snippet defining stages:

stages:
  - build
  - test
  - package
  - deploy

Step 2: Write your first job

Each stage contains jobs. Begin with a simple build job that runs on any runner.

build_job:
  stage: build
  script:
    - echo "Compiling the application..."
    - # Insert build commands, e.g. mvn package, npm run build, etc.
  artifacts:
    paths:
      - build/
    expire_in: 1 hour

Artifacts preserve outputs between stages. Adjust paths as per your project structure.

Step 3: Add a test job

Testing is vital. Introduce a job in the test stage to validate your code.

test_job:
  stage: test
  script:
    - echo "Running tests..."
    - # Example for JavaScript: npm test
  needs:
    - build_job
  allow_failure: false
  coverage: '/Coverage:s+d+%/'  # Optional, if you generate coverage reports

The needs keyword optimises pipeline concurrency by allowing dependent jobs to run sooner. Use it judiciously for complex DAGs.

Step 4: Package and deploy steps

Your package job bundles executables, and deploy jobs will optionally push code to environments.

package_job:
  stage: package
  script:
    - echo "Packaging application..."
    - # Package commands, e.g. zip, docker build, etc.
  needs:
    - test_job
  artifacts:
    paths:
      - dist/
    expire_in: 2 hours

deploy_staging:
  stage: deploy
  script:
    - echo "Deploying to staging server..."
    - # Deployment commands, like SSH or Kubernetes kubectl apply
  needs:
    - package_job
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - main  # or your production branch

Use the environment keyword for tracking, review apps, and auto-stop features.

Step 5: Implement CI variables and secrets management

Manage sensitive data like API keys or deploy tokens via GitLab CI/CD variables in your project settings rather than hardcoding. Example referencing a variable in a script:

deploy_staging:
  stage: deploy
  script:
    - echo "Deploying with secret key"
    - deploy-scripts --key "$DEPLOY_KEY"
  variables:
    DEPLOY_KEY: $DEPLOY_KEY  # Retrieved from CI/CD settings

Common pitfalls

  • Not locking runner tags: Specify tags in jobs if using custom runners; jobs will stall otherwise.
  • Large artifacts: Avoid storing extremely large binaries; this can slow down your pipeline and consume runner storage.
  • Missing cache usage: Use cache: to speed up dependency installs; not caching may slow subsequent builds.
  • Infinite loops: Avoid triggering pipelines on branch/tag patterns that cause recursive runs (e.g. Docker image pushes triggering pipelines).
  • Ignoring pipeline status: Failing pipelines should block merge requests; enforce this via branch protection rules.

Validation

Once your .gitlab-ci.yml is committed, GitLab automatically parses and validates the syntax on push:

  • Check pipeline status in the “CI/CD > Pipelines” page.
  • Expand the job logs for real-time output and error tracking.
  • Use GitLab’s built-in CI Lint tool at https://gitlab.com/-/ci/lint to pre-validate your config before pushing.

Confirm environment URLs are correct and access logs during deployment. Configure notifications to alert failures promptly.

Checklist / TL;DR

  • Define clear, logical stages to represent your workflow
  • Create succinct jobs with meaningful names and scripts
  • Use artifacts and cache wisely to preserve and speed up state
  • Leverage needs for parallel execution and speed
  • Secure secrets with CI/CD variables; never commit them in code
  • Use environment keyword to manage deployment lifecycle
  • Validate and test pipelines often using GitLab’s lint and UI status
  • Plan runners capacity — consider shared vs. private runners depending on build load and security
  • Use branch rules and validations to enforce code quality gates

References

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Post