Automate all the things with CI/CD in GitHub Actions

A presentation at phpday 2024 in May 2024 in Verona, VR, Italy by Rob Allen

Slide 1

Slide 1

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

Slide 2

Slide 2

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

Slide 3

Slide 3

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

Slide 4

Slide 4

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

Slide 5

Slide 5

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

Slide 6

Slide 6

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

Slide 7

Slide 7

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

Slide 8

Slide 8

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

Slide 9

Slide 9

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

Slide 10

Slide 10

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

Slide 11

Slide 11

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

Slide 12

Slide 12

.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

Slide 13

Slide 13

.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

Slide 14

Slide 14

.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

Slide 15

Slide 15

Events Rob Allen | social.akrabat.com/rob

Slide 16

Slide 16

.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

Slide 17

Slide 17

.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

Slide 18

Slide 18

Success Rob Allen | social.akrabat.com/rob

Slide 19

Slide 19

Failure Rob Allen | social.akrabat.com/rob

Slide 20

Slide 20

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

Slide 21

Slide 21

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

Slide 22

Slide 22

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

Slide 23

Slide 23

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

Slide 24

Slide 24

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

Slide 25

Slide 25

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

Slide 26

Slide 26

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

Slide 27

Slide 27

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

Slide 28

Slide 28

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

Slide 29

Slide 29

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

Slide 30

Slide 30

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

Slide 31

Slide 31

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

Slide 32

Slide 32

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

Slide 33

Slide 33

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

Slide 34

Slide 34

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

Slide 35

Slide 35

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

Slide 36

Slide 36

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

Slide 37

Slide 37

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

Slide 38

Slide 38

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

Slide 39

Slide 39

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

Slide 40

Slide 40

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

Slide 41

Slide 41

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

Slide 42

Slide 42

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

Slide 43

Slide 43

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

Slide 44

Slide 44

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

Slide 45

Slide 45

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

Slide 46

Slide 46

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

Slide 47

Slide 47

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

Slide 48

Slide 48

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

Slide 49

Slide 49

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

Slide 50

Slide 50

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

Slide 51

Slide 51

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

Slide 52

Slide 52

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

Slide 53

Slide 53

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

Slide 54

Slide 54

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

Slide 55

Slide 55

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

Slide 56

Slide 56

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

Slide 57

Slide 57

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

Slide 58

Slide 58

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

Slide 59

Slide 59

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

Slide 60

Slide 60

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

Slide 61

Slide 61

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

Slide 62

Slide 62

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

Slide 63

Slide 63

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

Slide 64

Slide 64

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

Slide 65

Slide 65

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

Slide 66

Slide 66

“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

Slide 67

Slide 67

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