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
- Define a simple branching strategy
- main (production-ready)
- develop (integration of features ready for staging)
- feature/* (short-lived branches for new work)
- 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.
- Establish commit quality with client-side gates Goals
- Catch obvious issues before pushing
- Encourage meaningful messages
- 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.
- Local verification: reproducible test runs Goal
- 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'
- Lightweight CI: automated checks on push Goal
- 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.
- Build artifacts and lightweight deployment gates Goal
- Produce reproducible artifacts for deployment
- 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
- Stage and production deployment with Git as the control plane Goal
- Deploy confidently using minimal tooling
- 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
- Rollbacks and recoverability Strategies
- Keep previous artifact/images accessible (tagged by commit)
- Use Kubernetes rollout status checks or Docker compose restart policies as a quick rollback
- 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
- Observability and lightweight monitoring What to observe
- Build and test durations
- Test coverage trends
- 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
- curl -sSf http://your-app-domain/health || exit 1
- Security considerations
- Do not commit secrets. Use environment variables in CI and a secrets manager
- Rotate credentials periodically
Use least-privilege access for deployment tokens and registry credentials
A practical, step-by-step plan to start today
Step 1: Choose a simple branch and commit strategymain, develop, feature/*
Step 2: Implement pre-commit hooks locallySet up linting and formatting
Step 3: Create a minimal CI workflowA single job that runs install, lint, test, and build
Step 4: Add a basic staging deployment triggerDeploy when a release/vX.Y.Z tag is created
Step 5: Add a production deployment triggerDeploy when main is updated and CI passes
Step 6: Add basic monitoring and rollback planHealth 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
Top comments (0)