CI/CD With GitHub Actions — A Practical, End‑to‑End Tutorial

What Is CI/CD?

Continuous Integration (CI): automate build, lint, test, and packaging on every change.
Continuous Delivery/Deployment (CD): automate promotion to environments (staging → production), often with approvals, feature flags, and …


This content originally appeared on DEV Community and was authored by Goodluck Ekeoma Adiole

What Is CI/CD?

  • Continuous Integration (CI): automate build, lint, test, and packaging on every change.
  • Continuous Delivery/Deployment (CD): automate promotion to environments (staging → production), often with approvals, feature flags, and rollbacks.
  • Benefits: faster feedback, fewer regressions, reproducible releases, higher confidence.

CI/CD Tools at a Glance (Quick Compare)

  • Jenkins: highly extensible, self‑hosted; you manage controllers/agents and plugins.
  • GitLab CI: tightly integrated with GitLab; YAML pipelines, built‑in container registry.
  • Azure Pipelines: great for Microsoft stacks; Windows/macOS/Linux hosted pools.
  • CircleCI: cloud‑hosted, fast parallelism, orbs ecosystem.
  • Bitbucket Pipelines: simple pipelines for Bitbucket repos.
  • Buildkite: hybrid model; run builders on your infra.
  • Tekton: Kubernetes‑native pipelines (CRDs).
  • Argo CD / Flux: GitOps CD for Kubernetes (pull‑based).
  • Spinnaker / Harness: powerful multi‑cloud CD, canary/blue‑green baked in.

Why GitHub Actions? Native to GitHub, enormous marketplace, generous hosted runners, great DX, reusable workflows, and first‑class security integrations (OIDC, environments, approvals).

GitHub Actions Core Concepts

  • Workflow: YAML in .github/workflows/*.yml
  • Trigger (Event): push, pull_request, workflow_dispatch, schedule, release, etc.
  • Job: runs on a runner (runs-on: ubuntu-latest, windows-latest, etc.). Jobs can depend on others via needs.
  • Step: individual shell command or “action” (like actions/checkout).
  • Runners: GitHub‑hosted or self‑hosted (ephemeral or static).
  • Artifacts & Caching: persist build outputs; speed up installs.
  • Environments: dev, staging, prod with protection rules, approvals, and secrets.
  • Secrets & Variables: org/repo/environment scope; injected at runtime.
  • Permissions: least‑privilege via permissions: (and OIDC via id-token: write).

A Minimal CI Workflow (Node example)

Create .github/workflows/ci.yml:

name: CI

on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Use Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install
        run: npm ci

      - name: Lint
        run: npm run lint --if-present

      - name: Test
        run: npm test -- --ci --reporters=default --reporters=jest-junit

Notes:

  • Runs on PRs and on pushes to main.
  • Caches node_modules automatically via setup-node@v4 + cache: npm.

Python & Java Variants

Python (.github/workflows/python-ci.yml):

name: Python CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.10, 3.11, 3.12]
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
      - run: pip install -r requirements.txt
      - run: pytest -q --maxfail=1 --disable-warnings --junitxml=report.xml
      - uses: actions/upload-artifact@v4
        with:
          name: pytest-report
          path: report.xml

Java (Gradle):

name: Java CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21
          cache: gradle
      - run: ./gradlew build --stacktrace
      - uses: actions/upload-artifact@v4
        with:
          name: app-jar
          path: build/libs/*.jar

Service Containers for Integration Tests

Example: test against Postgres and Redis

name: Integration Tests
on: [push, pull_request]

jobs:
  itest:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: app
          POSTGRES_PASSWORD: password
          POSTGRES_DB: appdb
        ports: ["5432:5432"]
        options: >-
          --health-cmd="pg_isready -U app -d appdb"
          --health-interval=10s --health-timeout=5s --health-retries=5
      redis:
        image: redis:7

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: 3.11 }
      - run: pip install -r requirements.txt
      - env:
          DATABASE_URL: postgresql://app:password@localhost:5432/appdb
          REDIS_URL: redis://localhost:6379
        run: pytest tests/integration -q

Caching & Artifacts (Speed and Traceability)

  • Use setup actions’ built‑in caching (cache: npm/pip/gradle).
  • For custom caches:
- name: Cache build
  uses: actions/cache@v4
  with:
    path: .m2/repository
    key: maven-${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
    restore-keys: |
      maven-${{ runner.os }}-
  • Upload build outputs for later jobs or downloads:
- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/**
    if-no-files-found: error

CD Basics: Environments, Approvals, and Secrets

  1. Create environments in GitHub: dev, staging, prod.
  2. Add environment secrets/variables (e.g., PROD_DB_URL).
  3. Configure protection rules (approvers, wait timers).

Sample CD job gated by an environment:

jobs:
  deploy_prod:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: prod
      url: https://app.example.com
    permissions:
      contents: read
      id-token: write  # for OIDC to cloud providers
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: ./scripts/deploy.sh

Approvers receive a prompt in the PR/Actions UI before the job runs.

Multi‑Stage CI/CD (Build → Test → Deploy)

name: Webapp CI/CD

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  id-token: write

concurrency:
  group: app-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=sha
            type=semver,pattern={{version}}
      - name: Build & Push
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

  test:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  deploy_staging:
    runs-on: ubuntu-latest
    needs: [test]
    environment: staging
    steps:
      - name: Deploy to Staging
        run: ./scripts/deploy_staging.sh ${{ needs.build.outputs.image }}

  deploy_prod:
    runs-on: ubuntu-latest
    needs: [deploy_staging]
    environment: prod
    steps:
      - name: Deploy to Production
        run: ./scripts/deploy_prod.sh ${{ needs.build.outputs.image }}

Cloud Deployments via OIDC (No Long‑Lived Secrets)

AWS (ECS/EKS/Lambda/etc.)

- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GHActionsDeployRole
    aws-region: eu-west-1

- name: Deploy infra with Terraform
  run: |
    terraform init
    terraform apply -auto-approve

- name: Update ECS service
  run: aws ecs update-service --cluster app --service web --force-new-deployment

Prereq: set IAM role with a trust policy allowing GitHub’s OIDC provider and your repo/environment.

Azure (Web Apps/AKS)

- uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Deploy to Azure Web App
  uses: azure/webapps-deploy@v3
  with:
    app-name: my-webapp
    images: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}

Use Azure Federated Credentials for your repo/environment to avoid client secret rotation.

GCP (Cloud Run/GKE)

- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: projects/123/locations/global/workloadIdentityPools/gh/providers/github
    service_account: gha-deployer@myproj.iam.gserviceaccount.com

- name: Deploy to Cloud Run
  run: |
    gcloud run deploy web --image=ghcr.io/${{ github.repository }}:${{ github.sha }} --region=europe-west1

Terraform in Actions (Infra as Code)

name: Terraform

on:
  pull_request:
    paths: ["infra/**.tf", ".github/workflows/terraform.yml"]
  push:
    branches: [main]
    paths: ["infra/**.tf"]

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform -chdir=infra init
      - run: terraform -chdir=infra plan -out=plan.out
      - uses: actions/upload-artifact@v4
        with:
          name: tf-plan
          path: infra/plan.out

  apply:
    if: github.ref == 'refs/heads/main'
    needs: plan
    environment: prod
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - uses: actions/download-artifact@v4
        with: { name: tf-plan, path: infra }
      - run: terraform -chdir=infra apply -auto-approve plan.out

Secure Supply Chain (SCA, SAST, Code Scanning)

  • Dependency Review on PRs:
name: Dependency Review
on: [pull_request]
permissions:
  contents: read
  pull-requests: write
jobs:
  dep-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/dependency-review-action@v4
  • CodeQL (static analysis):
name: CodeQL
on:
  push: { branches: [main] }
  pull_request:
  schedule: [{ cron: "35 1 * * 1" }] # weekly
permissions:
  contents: read
  security-events: write
jobs:
  analyze:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix: { language: [javascript, python, java] }
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with: { languages: ${{ matrix.language }} }
      - uses: github/codeql-action/analyze@v3
  • Optional: container provenance (SLSA‑style):
- name: Attest build provenance
  uses: actions/attest-build-provenance@v1
  with:
    subject-name: ghcr.io/${{ github.repository }}
    subject-digest: ${{ steps.meta.outputs.digest }}

Reusable Workflows & Composite Actions

Reusable workflow (publisher):

.github/workflows/reusable-test.yml

name: Reusable Test
on:
  workflow_call:
    inputs:
      node-version: { required: true, type: string }
jobs:
  run-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ inputs.node-version }}, cache: npm }
      - run: npm ci && npm test

Caller:

jobs:
  tests:
    uses: your-org/your-repo/.github/workflows/reusable-test.yml@main
    with:
      node-version: "20"

Composite action (encapsulate repeatable steps):

.github/actions/setup-app/action.yml

name: "Setup App"
runs:
  using: "composite"
  steps:
    - uses: actions/setup-node@v4
      with: { node-version: 20, cache: npm }
    - run: npm ci
      shell: bash

Use it:

- uses: ./.github/actions/setup-app

Monorepos & Path Filters

Run pipelines only when relevant code changes:

on:
  pull_request:
    paths:
      - "services/api/**"
      - ".github/workflows/api-*.yml"

jobs:
  api-ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make -C services/api test

Matrix by service:

strategy:
  matrix:
    service: [api, web, worker]
steps:
  - run: make -C services/${{ matrix.service }} test

Branching & Release Strategies

  • Trunk‑based (recommended): PRs → main; feature flags; short‑lived branches.
  • GitFlow: develop, release/*, hotfix/*; more ceremony.
  • Semver tags trigger releases:
on:
  push:
    tags: ["v*.*.*"]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Changelog
        run: npx conventional-changelog -p angular -i CHANGELOG.md -s
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: |
            dist/**

Advanced Controls & Guardrails

  • concurrency: to prevent overlapping deploys.
  • environment: with required reviewers and wait timers.
  • timeout-minutes: per job.
  • if: conditions (e.g., only deploy on tags).
  • needs: to enforce stage order.
  • permissions: minimal scopes; add id-token: write only when needed.
  • Pin actions to major versions or SHAs (for maximum supply‑chain safety).
  • Secret scanning & push protection: keep secrets out of code.

Self‑Hosted Runners (When and How)

Why: custom tools, private networks, GPUs, cost control, long builds.

Basic setup steps:

  1. Provision VM/container with network access to targets.
  2. Create runner in repo/org (Settings → Actions → Runners).
  3. Label runners (e.g., self-hosted, gpu, arm64).
  4. Prefer ephemeral/auto‑scaled runners (clean state per job).

Use in workflow:

jobs:
  build:
    runs-on: [self-hosted, linux, x64, docker]

Security tips:

  • Lock runners to specific repos.
  • Rotate tokens; auto‑update runner.
  • Isolate with VM snapshots or ephemeral images.

Slide 18 — Observability, Test Reports, Coverage, and Artifacts

  • Upload test reports & coverage to keep PR feedback rich.
  • Publish HTML reports as artifacts or Pages in non‑prod.
  • Example (Jest coverage):
- run: npm run test -- --coverage
- uses: actions/upload-artifact@v4
  with:
    name: coverage
    path: coverage/**
  • Annotate PRs via problem matchers or actions (eslint, flake8, etc.).

Scheduling, Manual Runs, and Branch Protections

  • Schedules use UTC (not repository timezone):
on:
  schedule:
    - cron: "0 5 * * 1-5" # 05:00 UTC on weekdays
  workflow_dispatch:
    inputs:
      target:
        description: "Env to deploy"
        required: true
        default: "staging"
  • Combine with branch protection rules (required checks before merge).

End‑to‑End Example: Dockerized API → Staging/Prod (ECS)

.github/workflows/api-cicd.yml:

name: API CI/CD

on:
  pull_request:
    paths: ["api/**", ".github/workflows/api-cicd.yml"]
  push:
    branches: [main]
    paths: ["api/**", ".github/workflows/api-cicd.yml"]

permissions:
  contents: read
  id-token: write

env:
  IMAGE_NAME: ghcr.io/${{ github.repository }}/api

jobs:
  ci:
    runs-on: ubuntu-latest
    outputs:
      image_tag: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.IMAGE_NAME }}
          tags: |
            type=sha,format=long
            type=ref,event=branch

      - name: Build & Push
        uses: docker/build-push-action@v6
        with:
          context: ./api
          push: true
          tags: ${{ steps.meta.outputs.tags }}

      - uses: actions/upload-artifact@v4
        with:
          name: image-tags
          path: |
            # emit a simple file with the final tag(s)
            /dev/stdout
        if: always()

  deploy_staging:
    needs: ci
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gh-ecs-deployer
          aws-region: eu-west-1
      - name: Update ECS service (staging)
        run: |
          aws ecs update-service \
            --cluster app-staging \
            --service api \
            --force-new-deployment

  deploy_prod:
    if: startsWith(github.ref, 'refs/tags/v')
    needs: deploy_staging
    runs-on: ubuntu-latest
    environment: prod
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gh-ecs-deployer
          aws-region: eu-west-1
      - name: Update ECS service (prod)
        run: |
          aws ecs update-service \
            --cluster app-prod \
            --service api \
            --force-new-deployment

Flow:

  • PR → CI only.
  • Push to main → build/push image + deploy to staging.
  • Create tag vX.Y.Z → deploy to prod (after staging job completes and environment approval, if configured).

Common Pitfalls & Troubleshooting

  • “Permission denied” on checkout/push: set permissions: contents: write when publishing tags or creating releases.
  • OIDC failing: verify correct audience/repo/environment in cloud role trust policy.
  • Slow builds: add caches, narrow paths, enable parallel matrix, use bigger runners (e.g., ubuntu-latest vs ubuntu-24.04 as available).
  • Flaky tests: add service health checks and retries; separate unit vs integration; use timeout-minutes.
  • Secrets not found: confirm secret scope (environment vs repo vs org) and name casing.

Security Best Practices Checklist

  • Least‑privilege permissions: per workflow/job.
  • Use environments for prod with required reviewers.
  • Prefer OIDC to cloud over static keys.
  • Pin actions to major versions or commit SHAs.
  • Keep runners ephemeral or routinely cleaned.
  • Enable branch protections + required status checks.
  • Turn on secret scanning, Dependabot alerts & updates.
  • Store sensitive config as environment secrets, not repo secrets if env‑specific.

Suggested Project Structure

.
├─ api/                      # your app(s)
├─ infra/                    # terraform/helm
├─ scripts/                  # deploy scripts
├─ .github/
│  ├─ actions/
│  │  └─ setup-app/          # composite action
│  └─ workflows/
│     ├─ ci.yml
│     ├─ codeql.yml
│     ├─ terraform.yml
│     └─ api-cicd.yml
└─ Dockerfile

Quick Start Checklist

  1. Add .github/workflows/ci.yml and run a PR.
  2. Add environments and secrets for staging and prod.
  3. Add OIDC role/federated credentials in your cloud.
  4. Implement build → test → deploy workflow with approvals.
  5. Add CodeQL + Dependency Review.
  6. Add path filters and caches.
  7. Monitor, iterate, and keep pipelines as code.

Copy‑Paste Starters (Grab‑Bag)

Manual Deploy with Inputs

on:
  workflow_dispatch:
    inputs:
      env:
        type: choice
        options: [staging, prod]
        default: staging

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.env }}
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy_${{ inputs.env }}.sh

Blue‑Green (Feature Flag‑Style) Toggle

- name: Flip production traffic
  run: ./scripts/switch_traffic.sh --to blue

Conditional Job (Only on PRs from internal repo)

if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}

What to Implement Next

  • Tracing CI duration and break‑down per step; set goals for speed.
  • Parallelize test suites via matrix/shards.
  • Add canary/percentage rollouts (ECS, Cloud Run, AKS/GKE).
  • GitOps for Kubernetes (Argo CD/Flux) and make Actions only push manifests/images.
  • DORA metrics (deployment frequency, lead time, MTTR, change‑fail rate) from Actions runs.


This content originally appeared on DEV Community and was authored by Goodluck Ekeoma Adiole


Print Share Comment Cite Upload Translate Updates
APA

Goodluck Ekeoma Adiole | Sciencx (2025-08-16T15:49:26+00:00) CI/CD With GitHub Actions — A Practical, End‑to‑End Tutorial. Retrieved from https://www.scien.cx/2025/08/16/ci-cd-with-github-actions-a-practical-end%e2%80%91to%e2%80%91end-tutorial/

MLA
" » CI/CD With GitHub Actions — A Practical, End‑to‑End Tutorial." Goodluck Ekeoma Adiole | Sciencx - Saturday August 16, 2025, https://www.scien.cx/2025/08/16/ci-cd-with-github-actions-a-practical-end%e2%80%91to%e2%80%91end-tutorial/
HARVARD
Goodluck Ekeoma Adiole | Sciencx Saturday August 16, 2025 » CI/CD With GitHub Actions — A Practical, End‑to‑End Tutorial., viewed ,<https://www.scien.cx/2025/08/16/ci-cd-with-github-actions-a-practical-end%e2%80%91to%e2%80%91end-tutorial/>
VANCOUVER
Goodluck Ekeoma Adiole | Sciencx - » CI/CD With GitHub Actions — A Practical, End‑to‑End Tutorial. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/16/ci-cd-with-github-actions-a-practical-end%e2%80%91to%e2%80%91end-tutorial/
CHICAGO
" » CI/CD With GitHub Actions — A Practical, End‑to‑End Tutorial." Goodluck Ekeoma Adiole | Sciencx - Accessed . https://www.scien.cx/2025/08/16/ci-cd-with-github-actions-a-practical-end%e2%80%91to%e2%80%91end-tutorial/
IEEE
" » CI/CD With GitHub Actions — A Practical, End‑to‑End Tutorial." Goodluck Ekeoma Adiole | Sciencx [Online]. Available: https://www.scien.cx/2025/08/16/ci-cd-with-github-actions-a-practical-end%e2%80%91to%e2%80%91end-tutorial/. [Accessed: ]
rf:citation
» CI/CD With GitHub Actions — A Practical, End‑to‑End Tutorial | Goodluck Ekeoma Adiole | Sciencx | https://www.scien.cx/2025/08/16/ci-cd-with-github-actions-a-practical-end%e2%80%91to%e2%80%91end-tutorial/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.