Automate all the things with CI/CD in GitHub Actions Rob Allen, May 2024

How do we test and release software? Rob Allen | social.akrabat.com/rob

Workflow to accept a code change 1. 2. 3. 4. 5. 6. 7. Checkout the source code Install dependencies Compile (or create container) Run code style checks Run tests Send artifacts (logs, test output, etc.) to dev for debugging Tell dev that it worked (or failed) Rob Allen | social.akrabat.com/rob

Workflow to release a new version 1. 2. 3. 4. 5. 6. Checkout the source code Compile (or create container) Upload container to registry (exe to Release) Deploy to container orchestration platform Publish release Notify Slack Rob Allen | social.akrabat.com/rob

We never get this right every time! Rob Allen | social.akrabat.com/rob

Humans are bad at repetitive tasks Rob Allen | social.akrabat.com/rob

Humans are bad at repetitive tasks That’s why we invented computers Rob Allen | social.akrabat.com/rob

Tests ensure our software works CI ensures that we run them CD releases it reliably Rob Allen | social.akrabat.com/rob

Our repository is the centre of our development world Rob Allen | social.akrabat.com/rob

GitHub Actions runs scripts when an event happens Rob Allen | social.akrabat.com/rob

YAML all the way down! sorry! Rob Allen | social.akrabat.com/rob

.github/workflows/ci.yml name: CI on: [push, pull_request] jobs: qa: name: QA checks runs-on: ubuntu-latest steps: - name: “Say Hello” run: echo “Hello World” - name: “Say Goodbye” run: echo “All done” Rob Allen | social.akrabat.com/rob

.github/workflows/ci.yml name: CI on: [push, pull_request] jobs: qa: name: QA checks runs-on: ubuntu-latest steps: - name: “Say Hello” run: echo “Hello World” - name: “Say Goodbye” run: echo “All done” Rob Allen | social.akrabat.com/rob

.github/workflows/ci.yml name: CI on: [push, pull_request] jobs: qa: name: QA checks runs-on: ubuntu-latest steps: - name: “Say Hello” run: echo “Hello World” - name: “Say Goodbye” run: echo “All done” Rob Allen | social.akrabat.com/rob

Events Rob Allen | social.akrabat.com/rob

.github/workflows/ci.yml name: CI on: [push, pull_request] jobs: qa: name: QA checks runs-on: ubuntu-latest steps: - name: “Say Hello” run: echo “Hello World” - name: “Say Goodbye” run: echo “All done” Rob Allen | social.akrabat.com/rob

.github/workflows/ci.yml name: CI on: [push, pull_request] jobs: qa: name: QA checks runs-on: ubuntu-latest steps: - name: “Say Hello” run: echo “Hello World” - name: “Say Goodbye” run: echo “All done” Rob Allen | social.akrabat.com/rob

Success Rob Allen | social.akrabat.com/rob

Failure Rob Allen | social.akrabat.com/rob

PHP quality checks Rob Allen | social.akrabat.com/rob

Set up the pipeline name: PHP Checks on: [pull_request] jobs: php-checks: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Create .env file run: cp .env.ci .env Rob Allen | social.akrabat.com/rob

Set up the pipeline name: PHP Checks on: [pull_request] jobs: php-checks: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Create .env file run: cp .env.ci .env Rob Allen | social.akrabat.com/rob

Set up the pipeline name: PHP Checks on: [pull_request] jobs: php-checks: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Create .env file run: cp .env.ci .env Rob Allen | social.akrabat.com/rob

Grab PHP - name: Install PHP uses: “shivammathur/setup-php@v2” with: coverage: “pcov” php-version: “8.3.4” tools: composer:v2, cs2pr Rob Allen | social.akrabat.com/rob

Dependencies - name: Run composer run: composer install —prefer-dist —no-progress —no-ansi —no-interaction - name: Install npm run: npm install Rob Allen | social.akrabat.com/rob

Code quality - name: Check code style run: vendor/bin/phpcs -q —report=checkstyle | cs2pr Rob Allen | social.akrabat.com/rob

Code quality - name: Check code style run: vendor/bin/phpcs -q —report=checkstyle | cs2pr - name: Run static analysis checks run: vendor/bin/phpstan analyse Rob Allen | social.akrabat.com/rob

Code quality - name: Check code style run: vendor/bin/phpcs -q —report=checkstyle | cs2pr - name: Run static analysis checks run: vendor/bin/phpstan analyse - name: Run unit tests run: vendor/bin/phpunit -c phpunit-ci.xml —testsuite=unit Rob Allen | social.akrabat.com/rob

Other checks - name: Check licenses of PHP dependencies # (see akrabat.com/check-licenses-of-composer-dependencies) run: php bin/check-licenses.php Rob Allen | social.akrabat.com/rob

Other checks - name: Check licenses of PHP dependencies # (see akrabat.com/check-licenses-of-composer-dependencies) run: php bin/check-licenses.php - name: Check we can cache routes run: php bash -c “php artisan route:cache && php artisan route:clear” Rob Allen | social.akrabat.com/rob

Other checks - name: Check licenses of PHP dependencies # (see akrabat.com/check-licenses-of-composer-dependencies) run: php bin/check-licenses.php - name: Check we can cache routes run: php bash -c “php artisan route:cache && php artisan route:clear” - name: Check tailwind-build has been run. run: npm run tailwind-build && [ -z “$(git status —porcelain)” ] Rob Allen | social.akrabat.com/rob

Use Docker? Run in Docker! - name: Docker Compose Pull run: docker compose pull Rob Allen | social.akrabat.com/rob

Use Docker? Run in Docker! - name: Docker Compose Pull run: docker compose pull # Cache Docker layers - uses: jpribyl/action-docker-layer-caching@v0.1.1 continue-on-error: true Rob Allen | social.akrabat.com/rob

Use Docker? Run in Docker! - name: Docker Compose Pull run: docker compose pull # Cache Docker layers - uses: jpribyl/action-docker-layer-caching@v0.1.1 continue-on-error: true - name: Start the containers run: docker compose up —build -d Rob Allen | social.akrabat.com/rob

Tests that need the database - name: Ensure MySQL is available # (uses raphaelahrens/wait-for-it) run: docker-compose exec -T php ./wait-for-it -t 10 db:3306 Rob Allen | social.akrabat.com/rob

Tests that need the database - name: Ensure MySQL is available # (uses raphaelahrens/wait-for-it) run: docker-compose exec -T php ./wait-for-it -t 10 db:3306 - name: Run migrations run: docker-compose exec -T php bash -c “php artisan migrate:fresh —seed” Rob Allen | social.akrabat.com/rob

Tests that need the database - name: Ensure MySQL is available # (uses raphaelahrens/wait-for-it) run: docker-compose exec -T php ./wait-for-it -t 10 db:3306 - name: Run migrations run: docker-compose exec -T php bash -c “php artisan migrate:fresh —seed” - name: Execute tests run: docker-compose exec -T vendor/bin/phpunit -c phpunit-ci.xml —testsuite=integration Rob Allen | social.akrabat.com/rob

Upload assets - name: Upload test output uses: actions/upload-artifact@v2 if: failure() with: name: failed-tests path: tests/output retention-days: 8 Rob Allen | social.akrabat.com/rob

Upload assets - name: Upload test output uses: actions/upload-artifact@v4 if: failure() with: name: failed-tests path: tests/output retention-days: 8 Rob Allen | social.akrabat.com/rob

Upload assets - name: Upload test output uses: actions/upload-artifact@v4 if: failure() with: name: failed-tests path: tests/output retention-days: 8 Rob Allen | social.akrabat.com/rob

Everything we run in CI we also run locally Rob Allen | social.akrabat.com/rob

Tag and Release Rob Allen | social.akrabat.com/rob

When a milestone is closed… on: milestone: types: [closed] Rob Allen | social.akrabat.com/rob

do a full checkout… steps: - name: Checkout code uses: actions/checkout@v3 with: ref: main fetch-depth: 0 Rob Allen | social.akrabat.com/rob

so we can create & push a tag… - name: Create Tag uses: rickstaa/action-create-tag@v1 id: create-tag with: tag: “${{ github.event.milestone.title }}” message: “Tag ${{ github.event.milestone.title }}” Rob Allen | social.akrabat.com/rob

and create a GitHub Release - name: Create GitHub Release uses: actions/github-script@v6 with: script: | await github.rest.repos.createRelease({ generate_release_notes: true, name: “${{github.event.milestone.title}}”, tag_name: “${{github.event.milestone.title}}” }); Rob Allen | social.akrabat.com/rob

and create a GitHub Release - name: Create GitHub Release uses: actions/github-script@v6 with: script: | await github.rest.repos.createRelease({ generate_release_notes: true, name: “${{github.event.milestone.title}}”, tag_name: “${{github.event.milestone.title}}” }); Rob Allen | social.akrabat.com/rob

along with a new Milestone - name: ‘Get next minor version’ id: semvers uses: “WyriHaximus/github-action-next-semvers@v1” with: version: ${{github.event.milestone.title}} - name: ‘Create new milestone’ uses: “WyriHaximus/github-action-create-milestone@v1” with: title: ${{ steps.semvers.outputs.patch }} env: GITHUB_TOKEN: “${{ secrets.GITHUB_TOKEN }}” Rob Allen | social.akrabat.com/rob

along with a new Milestone - name: ‘Get next minor version’ id: semvers uses: “WyriHaximus/github-action-next-semvers@v1” with: version: ${{github.event.milestone.title}} - name: ‘Create new milestone’ uses: “WyriHaximus/github-action-create-milestone@v1” with: title: ${{ steps.semvers.outputs.patch }} env: GITHUB_TOKEN: “${{ secrets.GITHUB_TOKEN }}” Rob Allen | social.akrabat.com/rob

along with a new Milestone - name: ‘Get next minor version’ id: semvers uses: “WyriHaximus/github-action-next-semvers@v1” with: version: ${{github.event.milestone.title}} - name: ‘Create new milestone’ uses: “WyriHaximus/github-action-create-milestone@v1” with: title: ${{ steps.semvers.outputs.patch }} env: GITHUB_TOKEN: “${{ secrets.GITHUB_TOKEN }}” Rob Allen | social.akrabat.com/rob

along with a new Milestone - name: ‘Get next minor version’ id: semvers uses: “WyriHaximus/github-action-next-semvers@v1” with: version: ${{github.event.milestone.title}} - name: ‘Create new milestone’ uses: “WyriHaximus/github-action-create-milestone@v1” with: title: ${{ steps.semvers.outputs.patch }} env: GITHUB_TOKEN: “${{ secrets.GITHUB_TOKEN }}” Rob Allen | social.akrabat.com/rob

Compile & upload binaries Rob Allen | social.akrabat.com/rob

When a release is published… on: release: types: - published Rob Allen | social.akrabat.com/rob

build the binaries… steps: # checkout, setup Go etc… - name: Build the Rodeo executables # (akrabat.com/building-go-binaries-for-different-platforms) run: ./build-exes.sh ${{ github.ref_name }} Rob Allen | social.akrabat.com/rob

and upload them - name: Upload the Rodeo binaries uses: actions/svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref }} file: ./release/rodeo-* file_glob: true Rob Allen | social.akrabat.com/rob

Build and push to ECR Rob Allen | social.akrabat.com/rob

Build container… on: release: types: - published Rob Allen | social.akrabat.com/rob

Build container… on: release: types: - published steps: # checkout, etc…

  • name: Build Docker Image run: docker build —tag img-name:${{ github.ref_name }} . Rob Allen | social.akrabat.com/rob

and push to ECR - name: Push to ECR uses: jwalton/gh-ecr-push@v1 with: access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} region: us-east-2 local-image: img-name:${{ github.ref_name }} image: img-name:${{ github.ref_name }}, img-name:latest Rob Allen | social.akrabat.com/rob

and push to ECR - name: Push to ECR uses: jwalton/gh-ecr-push@v1 with: access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} region: us-east-2 local-image: img-name:${{ github.ref_name }} image: img-name:${{ github.ref_name }}, img-name:latest Rob Allen | social.akrabat.com/rob

and push to ECR - name: Push to ECR uses: jwalton/gh-ecr-push@v1 with: access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} region: us-east-2 local-image: img-name:${{ github.ref_name }} image: img-name:${{ github.ref_name }}, img-name:latest Rob Allen | social.akrabat.com/rob

More! Rob Allen | social.akrabat.com/rob

More! • • • • • • Secrets live in GitHub, not git! Use conditionals to save time & resources Don’t like bash? Use Python with shell: python The GitHub cli (gh) is preinstalled Building a library? Use matrices to test on multiple PHPs Pre-built: https://github.com/marketplace?type=actions Rob Allen | social.akrabat.com/rob

Laminas Automatic Releases Rob Allen | social.akrabat.com/rob

To sum up Rob Allen | social.akrabat.com/rob

“a deployment pipeline is an automated manifestation of your process for getting software from version control into the hands of your users.” David Farley Rob Allen | social.akrabat.com/rob

Thank you! Rob Allen | social.akrabat.com/rob