Git Workflow and GitHub Actions CI/CD: From Commit to Production
The difference between a hobby project and a professional one often comes down to workflow automation. GitHub Actions lets you automate testing, linting, security scanning, and deployment — all from your repository.
Git Branching Strategy
GitHub Flow (Simple, Recommended for Most Teams)
main ──────────────────────────────────────────────────────→
│ │ │
├── feature/login ──→│ │
│ │ │
│ ── feature/cart ──→ │
│ │
│ ── fix/auth-bug ──→
Rules:
mainis always deployable- All work happens in feature branches
- Branches are merged via Pull Requests
- Deploy after every merge to main
Git Flow (Complex, for Release-Based Products)
main ──────────────────────────────────────────────────────→
│ │
develop ──────────────────────────────────────────→ │
│ │ │ │
│ feature/a ──→ │ │
│ feature/b ──→ │
│ release/1.2 ──→ │
│ hotfix/critical ──→
Branch Naming Conventions
# Feature branches
feature/user-authentication
feature/JIRA-123-shopping-cart
# Bug fixes
fix/login-redirect-loop
bugfix/memory-leak-in-worker
# Releases
release/v2.1.0
release/2026-Q1
# Hotfixes
hotfix/critical-security-patch
hotfix/payment-gateway-down
# Chores
chore/update-dependencies
chore/add-eslint-rules
refactor/migrate-to-typescript
docs/update-api-documentation
Commit Message Convention (Conventional Commits)
# Format: type(scope): description
feat(auth): add OAuth2 Google login
fix(cart): prevent duplicate item addition on rapid clicks
docs(api): update rate limiting documentation
chore(deps): upgrade to Node.js 20 LTS
refactor(utils): simplify date formatting functions
test(auth): add missing edge case tests for token expiry
perf(db): add index on orders.created_at column
ci(github): add automatic vulnerability scanning
# Breaking changes — add ! or footer
feat(api)!: change user endpoint response format
# OR
feat(api): change user endpoint response format
BREAKING CHANGE: user.fullName is now user.firstName + user.lastName
GitHub Actions Basics
# .github/workflows/my-workflow.yml
name: CI Pipeline
# When to trigger
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch: # Manual trigger button in GitHub UI
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest # or macos-latest, windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Cache node_modules for speed
- name: Install dependencies
run: npm ci # Faster than npm install, uses package-lock.json
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
Complete CI Pipeline
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm run lint
- run: npm run typecheck
test:
name: Test
runs-on: ubuntu-latest
needs: lint # Only run if lint passes
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Run migrations
run: npm run db:migrate
env:
DATABASE_URL: postgresql://user:password@localhost:5432/testdb
- name: Run tests with coverage
run: npm test -- --coverage
env:
DATABASE_URL: postgresql://user:password@localhost:5432/testdb
JWT_SECRET: test-secret-32-characters-long-xx
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Check for vulnerabilities
run: npm audit --audit-level=high
- name: Run Snyk scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: [test, security]
if: github.ref == 'refs/heads/main' # Only on main branch
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
myapp/api:latest
myapp/api:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Deploy to Production
# .github/workflows/deploy.yml
name: Deploy to Production
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
environment:
name: production
url: https://myapp.com # Shows in GitHub deployment
steps:
- uses: actions/checkout@v4
- name: Deploy to Railway
uses: railway-app/railway-action@v1
with:
token: ${{ secrets.RAILWAY_TOKEN }}
service: api
# OR deploy to your server
- name: Deploy to server via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /app
docker pull myapp/api:${{ github.sha }}
docker-compose up -d --no-deps api
docker system prune -f
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
generate_release_notes: true
Branch Protection Rules
# Enforce via GitHub Settings → Branches → Add rule
# Or via github CLI:
gh api repos/{owner}/{repo}/branches/main/protection -X PUT -f required_status_checks='{"strict":true,"contexts":["lint","test","security"]}' -f enforce_admins=true -f required_pull_request_reviews='{"required_approving_review_count":1,"dismiss_stale_reviews":true}' -f restrictions=null
Branch protection settings:
- ✅ Require pull request before merging
- ✅ Require status checks to pass (CI must be green)
- ✅ Require at least 1 approval
- ✅ Dismiss stale reviews on new commits
- ✅ Require linear history (no merge commits)
- ✅ Include administrators
Useful GitHub Actions Patterns
Matrix Testing
# Test across multiple Node versions
strategy:
matrix:
node: ['18', '20', '22']
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
Caching
# Cache node_modules (10x speedup)
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
Secrets Management
# Access secrets
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# Never echo secrets (GitHub masks them automatically)
- run: echo "JWT is ${{ secrets.JWT_SECRET }}" # Shows: JWT is ***
Summary
A professional workflow:
- Branch: Work in feature branches
- Commit: Use conventional commits
- PR: Open pull request with description
- CI: Lint + test + security check automatically
- Review: Get peer review
- Merge: Squash and merge to main
- Deploy: Auto-deploy to production
→ Reference common Git commands with the Git Memo tool.