DevOps

GitHub Actions & GitLab CI

Learn cloud-native CI/CD with GitHub Actions and GitLab CI. Create YAML pipelines, use built-in features, and integrate with cloud services.

By TechCoder TeamLast updated: 2026-06-02
In a Nutshell

Learn cloud-native CI/CD with GitHub Actions and GitLab CI. Create YAML pipelines, use built-in features, and integrate with cloud services. This hands-on tutorial focuses on practical implementation of github actions & gitlab ci concepts.

GitHub Actions & GitLab CI

Cloud-native CI/CD solutions integrate seamlessly with your Git repositories, providing powerful automation without managing infrastructure.

GitHub Actions

Workflow Structure

# .github/workflows/pipeline.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM
  workflow_dispatch:  # Manual trigger

env:
  NODE_VERSION: '18.x'
  REGISTRY: ghcr.io

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run linter
        run: npm run lint
        
      - name: Run tests
        run: npm test
        
      - name: Build application
        run: npm run build
        
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-files
          path: dist/

  security-scan:
    runs-on: ubuntu-latest
    needs: build
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          format: 'sarif'
          output: 'trivy-results.sarif'
          
      - name: Upload scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

  docker-build:
    runs-on: ubuntu-latest
    needs: [build, security-scan]
    permissions:
      contents: read
      packages: write
      
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        
      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=sha
            
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    runs-on: ubuntu-latest
    needs: docker-build
    environment: staging
    
    steps:
      - name: Deploy to Staging
        run: |
          echo "Deploying to staging..."
          # kubectl set image deployment/app app=ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment: production
    if: github.ref == 'refs/heads/main'
    
    steps:
      - name: Deploy to Production
        run: |
          echo "Deploying to production..."
          # Production deployment commands

Key Concepts

Workflows: YAML files in .github/workflows/

Events: Triggers for workflows

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - '!src/**/*.md'
  pull_request:
    types: [opened, synchronize, closed]
  release:
    types: [published]

Jobs: Parallelizable units of work

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16.x, 18.x, 20.x]
        os: [ubuntu-latest, windows-latest]
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

Actions: Reusable steps

steps:
  - uses: actions/checkout@v4      # Official action
  - uses: actions/setup-node@v4   # Setup environment
  - uses: codecov/codecov-action@v3  # Third-party
  - uses: ./.github/actions/custom   # Local action

Reusable Workflows

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      version:
        required: true
        type: string
    secrets:
      api_key:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - name: Deploy ${{ inputs.version }} to ${{ inputs.environment }}
        run: |
          echo "Deploying version ${{ inputs.version }}"
          # Deployment commands using ${{ secrets.api_key }}
# Call reusable workflow
jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      version: ${{ github.sha }}
    secrets:
      api_key: ${{ secrets.STAGING_API_KEY }}

Composite Actions

# .github/actions/setup/action.yml
name: 'Setup Environment'
description: 'Setup Node.js and install dependencies'

inputs:
  node-version:
    description: 'Node.js version'
    default: '18.x'
    required: false

runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
    
    - run: npm ci
      shell: bash
    
    - run: npm run validate
      shell: bash

GitLab CI

Pipeline Structure

# .gitlab-ci.yml
stages:
  - build
  - test
  - security
  - deploy

variables:
  NODE_VERSION: "18"
  DOCKER_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

# Global before_script
before_script:
  - echo "Running before every job"

# Job templates
.build_template: &build_definition
  stage: build
  image: node:$NODE_VERSION
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
  script:
    - npm ci
    - npm run build

build:app:
  <<: *build_definition
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

unit_tests:
  stage: test
  image: node:$NODE_VERSION
  needs: [build:app]
  script:
    - npm ci
    - npm test -- --coverage
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
      junit: test-results.xml

integration_tests:
  stage: test
  services:
    - postgres:15
    - redis:7
  variables:
    POSTGRES_DB: test
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
  script:
    - npm ci
    - npm run test:integration
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

security_scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --exit-code 0 --no-progress $CI_PROJECT_DIR
  allow_failure: true
  artifacts:
    reports:
      sast: gl-sast-report.json

docker_build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $DOCKER_IMAGE .
    - docker push $DOCKER_IMAGE
  only:
    - main
    - develop

deploy_staging:
  stage: deploy
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - kubectl config use-context staging
    - kubectl set image deployment/app app=$DOCKER_IMAGE
    - kubectl rollout status deployment/app
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

deploy_production:
  stage: deploy
  image: bitnami/kubectl:latest
  environment:
    name: production
    url: https://example.com
  when: manual
  allow_failure: false
  script:
    - kubectl config use-context production
    - kubectl set image deployment/app app=$DOCKER_IMAGE
    - kubectl rollout status deployment/app
  only:
    - main

Key Concepts

Stages: Sequential execution groups

stages:
  - build      # All 'build' stage jobs run first
  - test       # Then all 'test' stage jobs
  - deploy     # Finally 'deploy' stage jobs

Needs: DAG pipeline (skip stages)

job_a:
  stage: build
  
job_b:
  stage: test
  needs: [job_a]  # Run immediately after job_a, not waiting for entire build stage

Rules: Conditional job execution

deploy_prod:
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: always

Environments: Track deployments

deploy:
  environment:
    name: production
    url: https://$CI_ENVIRONMENT_SLUG.example.com
    on_stop: stop_environment
  script: ./deploy.sh

stop_environment:
  environment:
    name: production
    action: stop
  script: ./cleanup.sh
  when: manual

Comparison

FeatureGitHub ActionsGitLab CI
Config File.github/workflows/*.yml.gitlab-ci.yml
Reusable ConfigReusable workflows + Composite actionsinclude templates
Parallel Jobs
Matrix Buildsstrategy.matrixparallel: matrix
SecretsRepository + Environment secretsCI/CD variables + Vault
Self-hosted Runners✅ (GitLab Runner)
Container RegistryGitHub Container RegistryGitLab Registry
Artifacts
Caching
Approval GatesEnvironments with protection ruleswhen: manual + environments

Best Practices

Security

# GitHub Actions - Use OIDC instead of long-lived secrets
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789:role/my-role
    aws-region: us-east-1

# Pin actions to specific commits
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

Optimization

# Cache dependencies
- uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

# Conditional execution
- name: Deploy
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  run: ./deploy.sh

Quiz

Quiz

Question 1 of 5

In GitHub Actions, where must workflow files be located?

.ci/workflows/
.github/workflows/
.actions/
.pipelines/

Next Steps

Now let's dive into Docker—the foundation of modern containerization.