GitHub Actions: Complete CI/CD Guide (2026)

GitHub Actions is the most popular CI/CD platform for open-source and private repositories. It runs workflows defined as YAML files directly in your repository — no separate CI server to manage. This guide covers the full workflow authoring experience: triggers, jobs, matrix builds, Docker pipelines, AWS deployment with OIDC (no static credentials), and reusable workflows.

1. Workflow Anatomy

A complete workflow for a Java Spring Boot app:

name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  JAVA_VERSION: '21'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: temurin
          cache: maven
      - name: Run tests
        run: mvn test -B
      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: target/surefire-reports/

2. Triggers

on:
  # Push to specific branches
  push:
    branches: [main]
    paths-ignore: ['**.md', 'docs/**']

  # Pull request events
  pull_request:
    types: [opened, synchronize, reopened]
    branches: [main]

  # Manual trigger with inputs
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: staging
        type: choice
        options: [staging, production]

  # Scheduled (cron syntax — UTC)
  schedule:
    - cron: '0 2 * * 1-5'  # 2 AM UTC weekdays

  # Triggered by another workflow completing
  workflow_run:
    workflows: ["CI/CD Pipeline"]
    types: [completed]
    branches: [main]

3. Jobs and Steps

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}

    steps:
      - uses: actions/checkout@v4

      # Conditional step
      - name: Run only on main
        if: github.ref == 'refs/heads/main'
        run: echo "Deploying to production"

      # Multi-line run
      - name: Build and test
        run: |
          mvn clean package -DskipTests
          echo "Build completed: $(ls -la target/*.jar)"

      # Set output for use in other jobs
      - name: Set image tag
        id: meta
        run: echo "tags=myapp:${{ github.sha }}" >> $GITHUB_OUTPUT

  deploy:
    needs: build          # runs after build job
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production   # requires manual approval if configured
    steps:
      - name: Use build output
        run: echo "Deploying ${{ needs.build.outputs.image-tag }}"

4. Matrix Builds

jobs:
  test-matrix:
    strategy:
      fail-fast: false   # don't cancel other jobs if one fails
      matrix:
        java: ['17', '21']
        os: [ubuntu-latest, windows-latest]
        include:
          - java: '21'
            os: ubuntu-latest
            extra-args: '--enable-preview'
        exclude:
          - java: '17'
            os: windows-latest
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ matrix.java }}
          distribution: temurin
      - run: mvn test ${{ matrix.extra-args }}

5. Caching Dependencies

- uses: actions/cache@v4
  with:
    path: ~/.m2/repository
    key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
    restore-keys: |
      ${{ runner.os }}-maven-

# Node.js — use setup-node built-in cache
- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

# Python — use setup-python built-in cache
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

6. Docker Build and Push

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

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

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

7. Deploy to AWS with OIDC (No Static Credentials)

OIDC lets GitHub Actions authenticate to AWS using short-lived tokens — no AWS access keys stored in secrets:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # Required for OIDC
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
          aws-region: us-east-1

      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build, tag and push to ECR
        run: |
          IMAGE_URI="${{ steps.login-ecr.outputs.registry }}/myapp:${{ github.sha }}"
          docker build -t $IMAGE_URI .
          docker push $IMAGE_URI

      - name: Deploy to EKS
        run: |
          aws eks update-kubeconfig --name my-cluster --region us-east-1
          kubectl set image deployment/myapp myapp=$IMAGE_URI
          kubectl rollout status deployment/myapp
Note: Set up the IAM OIDC identity provider in AWS and create a role with condition token.actions.githubusercontent.com:sub: repo:myorg/myrepo:ref:refs/heads/main to restrict which branches can assume the role.

8. Reusable Workflows

# .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      java-version:
        type: string
        default: '21'
    secrets:
      SONAR_TOKEN:
        required: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ inputs.java-version }}
          distribution: temurin
      - run: mvn verify sonar:sonar
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

# Calling the reusable workflow:
# .github/workflows/main.yml
jobs:
  run-tests:
    uses: ./.github/workflows/reusable-test.yml
    with:
      java-version: '21'
    secrets:
      SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Frequently Asked Questions

GitHub Actions vs Jenkins — which should I use?

GitHub Actions if your code is on GitHub — zero infrastructure to manage, native integration, generous free tier. Jenkins if you need on-premise CI, complex custom logic, or you're not on GitHub. GitHub Actions has largely replaced Jenkins for cloud-native teams.

How do I store and use secrets in GitHub Actions?

Add secrets in Settings → Secrets and variables → Actions. Access via ${{ secrets.MY_SECRET }}. Secrets are masked in logs. For environment-specific secrets, use Environments with protection rules and required reviewers before production deployments.

How do I debug a failing workflow?

Enable debug logging by setting secret ACTIONS_STEP_DEBUG=true. Use tmate action for an interactive SSH session into the runner. Add continue-on-error: true to failing steps to see all output. Download artifacts for test reports.

What are GitHub Actions usage limits?

Free tier: 2,000 minutes/month on public repos (unlimited), 500 MB storage. Private repos: 2,000 minutes free then $0.008/min for Linux. Self-hosted runners are free but you manage the infrastructure.