Back

Sunday, 16. February 2025

GitHub Actions Are Great

This weekend was a hard one. I barely had any time working on personal projects or doing some writing. I was thinking of shifting this weekend's article one week back, but I want to get in the habit of consistent writing. So here we are. Today I want to talk about how I automated the deployment of my code for this blog and a few things I did to make the code a little more "stable," so to say. Let's dive in.

The Problem

Imagine you developed a nice project and want to put it out there. Of course, since you developed good habits as a developer, you have your code under version control with Git. You also use GitHub, like most devs do.

In order to put your project out there, you decide that you need a server and a domain. You set everything up, and thankfully, your hosting provider has an integration with GitHub. You just need to link the repository, and the server is automatically fetching the code from GitHub—nice.

But now, a few days later, you want to change something in the code, and you realize after pushing your changes to GitHub that you need to manually run a lot of stuff. For example, you need to pull the code from GitHub on the server, and from time to time, you also have some — let's say — weird bugs on the server. Pretty frustrating. Wouldn't it be great to have some kind of automation to update the code from GitHub to the server and ideally also run some "safety" checks? That's where GitHub Actions come into play.

The Solution - GitHub Actions

GitHub Actions are a way to automate tasks and let certain scripts or actions run on specific events. The most common example is automatically deploying code on the server when pushing to the develop or main branch. But there is a lot more you can do with GitHub Actions. For example, I ran a static code analyzer on every merge request. You could also run custom scripts on certain events. It's really powerful.

It is important to say that this kind of automation is, of course, not exclusive to GitHub. I use it on GitLab during work a lot, for example. But GitHub Actions has some pretty cool community features that make it incredibly easy to use. All you need to do is go to your repository and click on the "Actions" tab. There you can search for different pre-build actions that fit for your project.

My Implementation

For my project, I wanted to use GitHub Actions to automate several tasks:

  1. Deploy code to the dev and prod servers when code is pushed to the develop and main branches.
  2. Run a security audit for all packages used on every pull request.
  3. Run a static code analyzer on every pull request.
  4. Run all tests on every pull request.

To achieve this, I created two YAML files: one for deployment automation and one for pull request automation. Let's start with the deployment automation.

Automated Deployment

name: deploy
on:
  push:
    branches:
      - develop
      - main

jobs:
    deploy-dev:
        if: github.ref == 'refs/heads/develop'
        runs-on: ubuntu-latest

        steps:
          - name: Deploy to dev
            env:
              DEPLOY_DEV_WEBHOOK: ${{ secrets.FORGE_DEPLOY_DEV }}
            run: |
              curl "$DEPLOY_DEV_WEBHOOK"

    deploy-main:
        if: github.ref == 'refs/heads/main'
        runs-on: ubuntu-latest

        steps:
          - name: Deploy to main
            env:
              DEPLOY_MAIN_WEBHOOK: ${{ secrets.FORGE_DEPLOY_PROD }}
            run: |
              curl "$DEPLOY_MAIN_WEBHOOK"

In this file, I define an action named deploy that triggers on pushes to the main and develop branches. Two jobs, deploy-dev and deploy-main, are defined to handle deployments to the development and production servers, respectively. Each job uses a webhook to trigger the deployment process on the server.

Code Checker

This workflow involves more steps, as I want several jobs to run on each pull request. Here's the code:

name: code_checking

on:
  pull_request:
    types: [opened,reopened,synchronize]

jobs:
  composer_audit:
      runs-on: ubuntu-latest
      steps:
        - name: Checkout code
          uses: actions/checkout@v4
        - name: Set up PHP
          uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
          with:
            php-version: '8.3'
            extensions: intl, gd, pdo_sqlite, sqlite3, xdebug
        - name: Install Composer dependencies
          run: composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
        - name: Run composer-audit
          run: composer audit

  static_code_analysis:
    runs-on: ubuntu-latest
    needs: composer_audit
    steps:
        - uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
          with:
            php-version: '8.3'
        - uses: actions/checkout@v4
        - name: Copy .env
          run: php -r "file_exists('.env') || copy('.env.example', '.env');"
        - name: install composer dependencies
          run: composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
        - name: Run Larastan
          run: php ./vendor/bin/phpstan analyse --memory-limit=512M --no-progress

  run_tests:
    runs-on: ubuntu-latest
    needs: static_code_analysis
    steps:
        - uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
          with:
            php-version: '8.3'
        - uses: actions/checkout@v4
        - name: Copy .env
          run: php -r "file_exists('.env') || copy('.env.example', '.env');"
        - name: install composer dependencies
          run: composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
        - name: Set app key
          run: php artisan key:generate --env=testing
        - name: Directory Permissions
          run: chmod -R 777 storage bootstrap/cache
        - name: Create Database
          run: |
            mkdir -p database
            touch database/database.sqlite
        - name: Install Node.js
          uses: actions/setup-node@v4
          with:
            node-version: '20'
        - name: Install npm dependencies
          run: npm install
        - name: Run npm
          run: npm run build
        - name: Run tests
          env:
            DB_CONNECTION: sqlite
            DB_DATABASE: database/database.sqlite
          run: php artisan test --testsuite=Feature --log-junit=./report.xml

First, I define again when to run this action. On every pull request that is opened, reopened or where new code is pushed to (synchronize). Then I define three different jobs here:

  1. composer_audit - where I simply run composer audit which checks all used packages for any security breach.
  2. static_code_analysis - where I run larastan to check for any potential issue with the code in the repository
  3. run_tests - where I - yeah - run all the tests.

For each job I defined multiple steps which run after each other. Most of the steps, like the following piece of code:

...
- uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
  with:
    php-version: '8.3'
- uses: actions/checkout@v4
- name: Copy .env
  run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: install composer dependencies
  run: composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
...

are used to build the dependencies which are required for the following actions to run. Thankfully there are a lot of packages and setups already build by other people that you can use for your actions. Here I use the build from shivamathur which installs everything I need in order to run php.

In order to run the tests I need to run a few more preperation steps like installing npm packages and building my assets. Otherwise test cases that involve rendering any view will fail. And that's basically it. Nothing spectacular but incredibly helpful. This makes my code a lot less error-prone and takes away a chunk of manual work.

Possible Improvements

Of course these files are not perfect and I bet there is a ton to improve. One thing that I don't like right now is that for every job I need to install the dependencies again. I haven't dived deep into GitHub Actions but it would be nice if I could define some kind of "keep" or "shared storage" between these jobs. Or I could simply do all these steps in one job. But I like them being seperated. I will take a look in the near future and see what I can improve on these actions in order to make them run more efficiently.

Conclusion

Implementing GitHub Actions saved a lot of time for my project. Automating deployments and ensuring code quality through continuous integration is a nice step forward. By setting up automated workflows for deployment, security audits, static code analysis, and testing, I've made my development process more efficient and reliable. These automations not only save time but also reduce the risk of errors, allowing me to focus more on writing code and less on manual tasks.

While my current setup is fully functional, there's always room for improvement. I look forward to exploring caching strategies and optimizing job dependencies.

I highly recommend diving into GitHub Actions and experimenting with your own workflows. The community support and extensive documentation make it accessible for developers at all levels.

Automation is a powerful tool in modern software development. GitHub Actions provides a flexible and robust platform to implement it. So, why not give it a try and see how it can improve your projects?

Happy coding!

Coding

Comments

Login or Register to write comments and like posts.


GitHub LinkedIn

© 2025