DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

Deploying a Git-Based CI/CD Pipeline from Scratch: A Practical, Tool-lean Guide

Deploying a Git-Based CI/CD Pipeline from Scratch: A Practical, Tool-lean Guide

Deploying a Git-Based CI/CD Pipeline from Scratch: A Practical, Tool-lean Guide

What you’ll learn

  • How to design a lightweight, Git-centric CI/CD workflow that works with teams of any size
  • How to implement local development local-first checks, commit-quality gates, and reproducible builds
  • How to automate deployments to staging and production with minimal tooling
  • How to observe, roll back, and iterate on releases confidently

Overview
This tutorial walks through building a pragmatic, Git-driven workflow you can implement with minimal fuss. The goal is to keep the pipeline visible in your version control, easy to reason about, and resilient to common CI pitfalls. You’ll wire together small, fast steps that run locally as you develop, and automatically when you push or merge, without locking you into a heavy platform.

Assumptions

  • You’re comfortable with Git basics: branches, commits, pushes, and merges
  • The project is a typical web service or app (Node, Python, Go, etc.) with a test suite
  • You don’t want to depend on a complex CI provider right away; you can augment later if needed
  1. Define a simple branching strategy
  2. main (production-ready)
  3. develop (integration of features ready for staging)
  4. feature/* (short-lived branches for new work)
  5. hotfix/* (urgent fixes on production)

Rules

  • All features start from develop.
  • All changes merged into develop trigger the CI steps designed below.
  • When ready for release, merge develop into main.

Reasoning
A small, predictable flow reduces context switching and makes automations easier to reason about. It also mirrors common Git workflows while staying approachable for teams new to CI/CD.

  1. Establish commit quality with client-side gates Goals
  2. Catch obvious issues before pushing
  3. Encourage meaningful messages
  4. Keep history readable

Tools and setup

  • Use a pre-commit hook to enforce formatting and linting
  • Use commit-msg hook to ensure conventional commits or a lightweight standard

Example: Husky and lint-staged (Node example)

  • Install (assuming a Node project):

    • npm install save-dev husky lint-staged
    • npx husky install
  • Package.json scripts

    • "lint": "eslint . max-warnings=0",
    • "format": "prettier write ."
  • .husky/pre-commit

    • npx lint-staged
  • lint-staged configuration in package.json

    • "lint-staged": { ".js": ["eslint fix", "git add"], ".ts": ["eslint fix", "git add"], "*.css": ["stylelint fix", "git add"] }
  • Commit message guidance

    • Use Conventional Commits format: feat:, fix:, docs:, chore:, etc.
    • Example: feat(auth): add multi-factor login

Alternative: for non-JS projects, adopt a lightweight commit-msg script that checks for a ticket ID and a short summary.

  1. Local verification: reproducible test runs Goal
  2. Ensure tests run consistently on every developer machine before pushing

Approach

  • Use a Makefile or a small runner to centralize commands
  • Seed the environment so tests aren’t flaky

Example (Makefile)

  • test:
    • @echo "Running test suite..."
    • npm test || exit 1
  • lint:
    • @echo "Running linter..."
    • npm run lint || exit 1
  • build:
    • @echo "Building artifacts..."
    • npm run build || exit 1

Add a local test script

  • scripts/local-test.sh
    • #!/usr/bin/env bash
    • set -euo pipefail
    • npm ci silent
    • npm run lint
    • npm test

Hook it up

  • Run: bash scripts/local-test.sh
  • Optional: alias t='bash scripts/local-test.sh'
  1. Lightweight CI: automated checks on push Goal
  2. Provide automated checks that run in a predictable, vendor-agnostic way

Tool choices

  • GitHub Actions (free tiers; easy for open-source or small teams)
  • GitLab CI or GitHub Actions equivalents if you’re on other platforms

Minimal GitHub Actions workflow

  • Create .github/workflows/ci.yml

Content outline

  • Trigger on push to develop and on pull_request against develop
  • Jobs:
    • setup: use a matrix to cover Node, Python, or Go if needed
    • test: install dependencies and run tests
    • lint: run lints
    • build: produce a lightweight artifact (e.g., a Docker image or a dist directory)

Example ci.yml (Node-focused)

  • name: CI on: push: branches: [ develop ] pull_request: branches: [ develop ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Use Node uses: actions/setup-node@v4 with: node-version: '18' - name: Install run: npm ci - name: Lint run: npm run lint - name: Test run: npm test - name: Build run: npm run build if-present - name: Create artifact if: success() run: echo "Artifact would be stored here"

Notes

  • Keep dependencies lightweight to speed up feedback
  • If you have multiple ecosystems, your CI job can be a matrix containing Node, Python, etc.
  1. Build artifacts and lightweight deployment gates Goal
  2. Produce reproducible artifacts for deployment
  3. Gate deployments on successful CI checks and on manual approvals if desired

Artifacts

  • For web apps: a dist directory or a Docker image
  • For libraries: a packaged artifact (tarball, wheel, or npm package)

Example: Docker-based artifact

  • Dockerfile (simple Python example)

    • FROM python:3.11-slim
    • WORKDIR /app
    • COPY . .
    • RUN pip install -r requirements.txt
    • CMD ["python", "app.py"]
  • Build locally

    • docker build -t my-app:dev .
  • Push strategy

    • Optional: push to a private registry or a registry mirror

Git-based gating

  • As part of CI, build a image tag with the commit hash: my-app:dev-abcdef
  • Ensure the image is stored as an artifact in CI or pushed to a registry if credentials exist
  1. Stage and production deployment with Git as the control plane Goal
  2. Deploy confidently using minimal tooling
  3. Tie deployment to Git events (e.g., push to develop → staging, merge develop to main → production)

Staging workflow

  • On merge develop into main? No, staging should come from a release branch or from develop merged into staging
  • Implement a staging deployment trigger:
    • Create a tag or a branch named release/vX.Y.Z on push
    • CI detects tag and deploys to staging

Production workflow

  • On merge to main, deploy to production
  • Optional: require a manual approval step before production deployment

Implementation pattern (Git-driven)

  • A deploy script that reads environment variables for target (staging vs production)
  • A simple example with Docker and a Kubernetes-like flavor
    • Use kubectl to apply manifests or a small script to update a deployment

Deployment script snippet (bash)

  • #!/usr/bin/env bash
  • set -euo pipefail
  • ENV=${1:-staging}
  • IMAGE_TAG=${2:-$GITHUB_SHA}
  • if [ "$ENV" = "production" ]; then
    • echo "Deploying to production..."
    • kubectl set image deployment/my-app my-app=my-app:$IMAGE_TAG
  • else
    • echo "Deploying to staging..."
    • kubectl set image deployment/my-app my-app=my-app:$IMAGE_TAG
  • fi

How you trigger

  • Staging: on push to release/vX.Y.Z or on tag vX.Y.Z
  • Production: on push to main after CI passes
  1. Rollbacks and recoverability Strategies
  2. Keep previous artifact/images accessible (tagged by commit)
  3. Use Kubernetes rollout status checks or Docker compose restart policies as a quick rollback
  4. Maintain a simple “last known good” artifact reference, e.g., an explicit tag or image digest

Roll-back example (Docker)

  • You can revert by reapplying a previous image tag:
    • kubectl set image deployment/my-app my-app=my-app:abcdef
  1. Observability and lightweight monitoring What to observe
  2. Build and test durations
  3. Test coverage trends
  4. Deployment success/failure and time-to-availability

Minimal observability approach

  • Emit CI job status into a shared channel (Slack, Discord, or email)
  • Include test coverage percentage and flaky-test indicators
  • Post-deployment health check endpoints: /health or /ready

Simple health check script

  1. Security considerations
  2. Do not commit secrets. Use environment variables in CI and a secrets manager
  3. Rotate credentials periodically
  4. Use least-privilege access for deployment tokens and registry credentials

  5. A practical, step-by-step plan to start today
    Step 1: Choose a simple branch and commit strategy

  6. main, develop, feature/*
    Step 2: Implement pre-commit hooks locally

  7. Set up linting and formatting
    Step 3: Create a minimal CI workflow

  8. A single job that runs install, lint, test, and build
    Step 4: Add a basic staging deployment trigger

  9. Deploy when a release/vX.Y.Z tag is created
    Step 5: Add a production deployment trigger

  10. Deploy when main is updated and CI passes
    Step 6: Add basic monitoring and rollback plan

  11. Health checks and a tag-based rollback path

Illustrative example: a tiny, end-to-end flow

  • A developer creates a feature/new-auth flow on a feature branch
  • They run local tests and ensure lint passes
  • They push; pre-commit hooks run locally
  • CI runs on develop or the feature branch, depending on policy
  • On feature completion, a release tag v1.0.0 is created from develop
  • CI builds a Docker image and pushes it to a registry
  • A staging deployment script uses the image tag to roll out to staging
  • After manual validation, main is updated; production deployment triggers automatically
  • If something fails, roll back to the previous image tag

Code snippets recap

  • Git commands (high level)
    • git checkout -b feature/new-auth
    • git commit -m "feat(auth): implement passwordless login"
    • git push origin feature/new-auth
    • git checkout develop
    • git merge no-ff feature/new-auth
    • git tag v1.0.0
    • git push origin v1.0.0
  • CI workflow skeleton (GitHub Actions)
    • name: CI
    • on: push: branches: [ develop ]; pull_request: branches: [ develop ]
    • jobs: test, lint, build
  • Deployment script usage
    • ./deploy.sh staging
    • ./deploy.sh production

What to customize for your context

  • Language and build steps: replace Node examples with Python, Go, Ruby, etc.
  • Artifacts: decide between Docker images, tarballs, or platform-specific packages
  • Deployment target: Kubernetes, ECS, VMs, or a simple host-based script

Would you like me to tailor this plan to your current stack (language, hosting, and CI provider) and draft concrete configuration files (CI YAML, Makefile, deployment scripts) you can drop into a repo? If so, tell me your tech stack and preferred tooling, and I’ll generate a complete, copy-paste starter suite.

-

Rizwan Saleem | https://rizwansaleem.co

Sources

Top comments (0)