Automate all the things with CI/CD in GitHub Actions Rob Allen, April 2024
A presentation at PHPSW, April 2024 in April 2024 in Bristol, UK by Rob Allen
Automate all the things with CI/CD in GitHub Actions Rob Allen, April 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: master 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
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…
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
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