0Pricing
PHP Academy · Lesson

CI/CD with GitHub Actions

Test and deploy PHP automatically on every push.

CI/CD with GitHub Actions is a free PHP Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

CI/CD for PHP

Every push should be tested, linted, statically analyzed, and — when green on the main branch — built into an image and deployed. GitHub Actions runs this pipeline on managed runners triggered by repo events.

We'll build a workflow that runs PHPUnit on a real MySQL service, caches Composer, runs PHPStan, builds a Docker image, and deploys.

Workflow Anatomy

A workflow lives in .github/workflows/*.yml. It has on: triggers, one or more jobs:, and each job has steps:. Jobs run on isolated runners in parallel unless linked by needs:.

name: CI
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

setup-php Action

shivammathur/setup-php is the standard way to install a specific PHP version with chosen extensions and tools (Composer, PHPStan, etc.) on the runner — far faster than building an image just to test.

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: pdo_mysql, intl, redis, zip
          coverage: pcov
          tools: composer:v2, phpstan

Caching Composer

Re-downloading dependencies on every run wastes minutes. Cache Composer's directory keyed on the composer.lock hash, so the cache invalidates only when dependencies change.

      - name: Get Composer cache dir
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: composer-

      - run: composer install --prefer-dist --no-progress

Service Containers

Jobs can spin up service containers — a real MySQL or Redis the runner can reach on 127.0.0.1. Add a healthcheck via options so steps don't run before the DB is ready.

  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.4
        env:
          MYSQL_DATABASE: app_test
          MYSQL_ROOT_PASSWORD: root
        ports: ['3306:3306']
        options: >-
          --health-cmd="mysqladmin ping -proot"
          --health-interval=5s --health-retries=10

Running Tests & Coverage

With deps installed and MySQL up, run PHPUnit. Point your test DSN at 127.0.0.1:3306. Generate coverage and optionally fail the build below a threshold.

      - name: Run PHPUnit
        env:
          DATABASE_URL: "mysql://root:root@127.0.0.1:3306/app_test"
        run: vendor/bin/phpunit --coverage-clover=coverage.xml

      - name: Static analysis
        run: phpstan analyse src --level=8 --no-progress

Matrix Builds

Libraries should pass on multiple PHP versions. A strategy.matrix fans the job out into parallel runs, one per combination, with ${{ matrix.php }} interpolated into the steps.

  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.2', '8.3', '8.4']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}

Computing the Image Tag

Deploys need a unique, traceable image tag. The commit SHA is the conventional choice. This snippet shows the tag-derivation logic you'd express in the workflow — turning a ref and SHA into a registry tag.

<?php
// Mirrors what the workflow computes for the image tag
$ref = 'refs/heads/main';
$sha = '9f41efadc0de1234567890abcdef0000deadbeef';

$branch = str_replace('refs/heads/', '', $ref);
$shortSha = substr($sha, 0, 7);
$tag = sprintf('registry.example.com/app:%s-%s', $branch, $shortSha);

echo $tag . PHP_EOL;          // registry.example.com/app:main-9f41efa
echo 'is_main: ' . ($branch === 'main' ? 'yes' : 'no') . PHP_EOL;
?>

Building & Pushing the Image

On the main branch, build the Docker image with docker/build-push-action using BuildKit and GitHub Actions cache. Log in to the registry with a secret token first; never hardcode credentials.

  build:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          target: runtime
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Secrets & OIDC

Store credentials in repository/environment secrets, referenced as ${{ secrets.NAME }} — they're masked in logs. For cloud deploys, prefer OIDC: the runner gets a short-lived token from AWS/GCP via permissions: id-token: write, so no long-lived keys live in the repo.

  deploy:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write     # enables OIDC
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy
          aws-region: eu-central-1

Deploy Step & Environments

Gate production deploys behind a GitHub environment (optionally with required reviewers). The deploy step then triggers your rollout — updating a Kubernetes deployment, ECS service, or SSHing to pull the new image.

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: production       # can require manual approval
      url: https://app.example.com
    steps:
      - name: Roll out
        run: |
          kubectl set image deployment/app \
            app=ghcr.io/${{ github.repository }}:${{ github.sha }}
          kubectl rollout status deployment/app --timeout=120s

Quick Check

What is the main security advantage of OIDC over stored cloud keys in Actions?

Recap

You built a PHP CI/CD pipeline in GitHub Actions: triggers on push/PR, setup-php with extensions, cached Composer keyed on the lockfile, a MySQL service container with healthcheck, PHPUnit + PHPStan, a version matrix, then a main-only build-push with GHA cache and an OIDC-authenticated, environment-gated deploy.

Principles: cache on lockfile hashes, gate ready services with healthchecks, tag images by SHA, and prefer OIDC over stored keys.

Frequently asked questions

Is the “CI/CD with GitHub Actions” lesson free?

Yes — the full text of “CI/CD with GitHub Actions” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.

What will I learn in “CI/CD with GitHub Actions”?

Test and deploy PHP automatically on every push. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start PHP Academy?

No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “CI/CD with GitHub Actions” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this PHP Academy lesson?

Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Containerizing a PHP Application
  2. Multi-Stage Builds and Optimization
  3. Docker Compose for Local Stacks
  4. CI/CD with GitHub Actions
← Back to PHP Academy