GitHub Actions for PHP Developers (HCL)

• 11 min read
Update 01.10.2019

The demo application mentioned in this article has been updated to work with the new YAML syntax. Checkout this Pull Request to learn what has changed.

Update 15.08.2019

On Augusth 8th GitHub announced some major updates to Actions. Switching from HCL to YAML syntax, CI/CD support and much more. This blog post was written before this announcement, so stuff might be different, depending on when you're reading this article.

If you're a developer using GitHub, you probably have heard of GitHub Actions. GitHub's new automated workflow system (similar to the CI feature of GitLab). It has been announced in October 2018 and is currently still in beta (You can sign up here to get access).

I recently got access to the beta and began developing Actions suited for my projects. As I currently spend most of my time writing code in PHP, the Actions mentioned in this article are focused on that language. The logic can be easily ported to other languages though.

A Workflow is a collection of multiple Actions which can be triggered by multiple GitHub webhook events (for example when code is pushed to the repository, a Pull Request has been merged or when a new Issue has been created)

An Action can basically do everything: run your test suite, publish a package to npm, deploy your site to your server, send a Slack message. You name it.
The code for an Action can live in the project repository itself, in a separate public GitHub repository or in a Docker Hub image.

Your Workflow is defined in the main.workflow file in the .github folder in your repository. This means your Actions are written in code and are in version control. If you like to work with a GUI, Workflows and Actions can also be configured and edited in a visual editor on github.com.

Screenshot of the Visual Editor for GitHub Actions on github.com
Screenshot of the Visual Editor for GitHub Actions on github.com

In this post I'm going to cover 3 Actions which I think could be useful for PHP developers in their daily workflows:

  • Run your phpunit test suite
  • Run phpinsights
  • Run php-cs-fixer

I've published a sample Laravel application, with all three Actions configured, on GitHub. You can clone it, fork it and see for yourself how the Actions are set up.
The process of adding those Actions is documented in this Pull Request.

As mentioned earlier, your Workflow and Actions are defined in a main.workflow file. The final file for our sample application looks like this:

workflow "phpunit / phpinsights / php-cs-fixer" {
  on = "push"
  resolves = [
    "phpunit",
    "phpinsights",
    "auto-commit-php-cs-fixer",
  ]
}

# Install composer dependencies
action "composer install" {
  uses = "MilesChou/composer-action@master"
  args = "install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist"
}

# Run phpunit testsuite
action "phpunit" {
  needs = ["composer install"]
  uses = "./actions/run-phpunit/"
  args = "tests/"
}

# Run phpinsights
action "phpinsights" {
  needs = ["composer install"]
  uses = "stefanzweifel/laravel-phpinsights-action@v1.0.0"
  args = "-v --min-quality=80 --min-complexity=80 --min-architecture=80 --min-style=80"
}

# Run php-cs-fixer
action "php-cs-fixer" {
  uses = "docker://oskarstark/php-cs-fixer-ga"
}

action "auto-commit-php-cs-fixer" {
  needs = ["php-cs-fixer"]
  uses = "stefanzweifel/git-auto-commit-action@v1.0.0"
  secrets = ["GITHUB_TOKEN"]
  env = {
    COMMIT_MESSAGE = "Apply php-cs-fixer changes"
    COMMIT_AUTHOR_EMAIL  = "jon.doe@example.com"
    COMMIT_AUTHOR_NAME = "Jon Doe"
  }
}

GitHub Actions are not written in YAML or JSON, but in HCL. If you've worked with Terraform in the past, the syntax might look familiar to you.

I won't go deep what each keyword in the Workflow syntax does (uses, needs, secrets, etc.).

The most important keywords for us right now are:

  • uses: Set which Action in a Workflow should be used
  • needs: Set which Action must successfully run, before this Action runs. (Similar to Dependencies)
  • env: Environment variables defined in the Workflow file itself. Allows you as a Action consumer to change things within an Action
  • secrets: Secret environment variables like API keys which should not be stored in the repository

Each Action is executed in a Docker container. Therefore, each Action needs a Dockerfile. If you now think: "Oh no! I don't know Docker!", then we have something in common. I've read and heard a lot about Docker over the years, but never really worked with it.

The good thing is that you don't have to be a Docker expert to create or work with Actions. All you need to do is set a base image and then you're good to go. The "core" Actions code can basically be written in any language.

One caveat you have to keep in mind when working with Actions, is that even though each Action is run in its own container, the underlying filesystem is shared with other Actions. Meaning: If one Action changes repository files, these changes are also available in other Actions.

So let's get started with our first PHP Action.

To do anything with a Laravel project, we first have to install its dependencies with composer.

This can be accomplished by using the general composer Action developed by MilesChou.

workflow "composer install" {
  on = "push"
  resolves = [
    "composer install"
  ]
}

action "composer install" {
  uses = "MilesChou/composer-action@master"
  args = "install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist"
}

As all following task depend on composer dependencies, all other task should have "composer install" defined in the needs keyword. The "composer install" Action is therefore executed before all other Actions. (GitHub is clever enough though, to only run the Action once).

One of the most common things to do in Continuous Integration is running your projects test suite on each code push.

As the test suite setup differs from project to project, I won't use a publicly available Action to run phpunit. I will use an Action defined in the project itself. In the root of my project I create an actions/run-phpunit-folder and within it the following files:

FROM php:7.3-cli-alpine

LABEL "com.github.actions.name"="phpunit-test"
LABEL "com.github.actions.description"="Run phpunit test suite"
LABEL "com.github.actions.icon"="check-circle"
LABEL "com.github.actions.color"="green"

ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
#!/bin/sh -l
set -eu

#  Setup Laravel App
cp .env.example .env
php artisan key:generate

#  Run phpunit Tests
vendor/bin/phpunit $*

In the Dockerfile we tell Actions that we want to use PHP 7.3 and that we want to execute the entrypoint.sh-file next.

The entrypoint.sh-file is where the logic of the Action lives. To run the test suite, we first create a copy of the example Laravel environment file and generate a fresh application key.

Next, phpunit is executed. Any provided arguments (args) are passed down and will be placed where the $* variable is.

If your project needs more PHP extensions or a MySQL database, the entrypoint.sh would be the place where you set these things up. (That's why I didn't use an already existing Action. The setup differs from project to project.)

In our Workflow file, we can now add the Action:

workflow "phpunit" {
  on = "push"
  resolves = [
    "phpunit"
  ]
}

action "composer install" {
  uses = "MilesChou/composer-action@master"
  args = "install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist"
}

action "phpunit" {
  needs = ["composer install"]
  uses = "./actions/run-phpunit/"
  args = "tests/"
}

Even though this was quite easy to set up, I personally wouldn't use GitHub Actions to run the test suite for my bigger projects yet.

Other CI services like Travis or Circle CI are much better suited for this task. These services give you richer notifications and better performance features like parallelism and dependency caching out of the box. With GitHub Actions you would have to implement this on your own.

However, I think Actions is perfectly fine for smaller projects.

Our next Action is going to run phpinsights on each push. As we don't have to set up a database or PHP extensions to run insights we can use an existing Action. As no phpinsights Actions existed when I wrote this article, I wrote my own: phpinsights-action and laravel-phpinsights-action.

As we're working with a Laravel app, we're going to use the Laravel version of the Action.

workflow "phpunit / phpinsights / php-cs-fixer" {
  on = "push"
  resolves = [
    "phpunit",
    "phpinsights"
  ]
}

# Other Actions

action "phpinsights" {
  needs = ["composer install"]
  uses = "stefanzweifel/laravel-phpinsights-action@v1.0.0"
  args = "-v --min-quality=80 --min-complexity=80 --min-architecture=80 --min-style=80"
}

As you can see, I'm passing the --min-xxx arguments to the Action. If the code quality of my app would drop in a future Pull Request, the Action would return a "failed" status code which in turn would mark the Pull Request as failed.

To see the reported issues of phpinsights you can open the log on github.com

Screenshot of PHP Insights output
Screenshot of PHP Insights output

Another common use case to use a CI service, is to check if your code follows the code convention and style guide you or your team has defined.

This can be accomplished by running an existing php-cs-fixer Action developed by Oskar Stark.

workflow "phpunit / phpinsights / php-cs-fixer" {
  on = "push"
  resolves = [
    "phpunit",
    "phpinsights",
    "php-cs-fixer",
  ]
}

# Other Actions

action "php-cs-fixer" {
  uses = "docker://oskarstark/php-cs-fixer-ga"
}

Here we've used another way to define an Action: By using the docker:// protocol, you can directly point to an image on DockerHub.

The Action uses your existing .php_cs configuration and runs php-cs-fixer on your project. However, the Action always returns a "successful" status code. It doesn't matter if violations happen.

Screenshot of Output of php-cs-fixer Action
Screenshot of Output of php-cs-fixer Action

But if violations would happen, php-cs-fixer automatically fixes them. So wouldn't it be cool if the fixed files would automatically be committed and pushed back to your branch? 🤔

As I said in the beginning of this article, the underlying file system is shared between multiple Actions in a Workflow. So committing the fixed files is just "an Action away".

I've created a git-auto-commit-Action which commits all changed files and pushes the commit back to the repository.

Our updated main.workflow file now looks like this.

workflow "phpunit / phpinsights / php-cs-fixer" {
  on = "push"
  resolves = [
    "phpunit",
    "phpinsights",
    "auto-commit-php-cs-fixer",
  ]
}

# Other Actions

action "php-cs-fixer" {
  uses = "docker://oskarstark/php-cs-fixer-ga"
}

action "auto-commit-php-cs-fixer" {
  needs = ["php-cs-fixer"]
  uses = "stefanzweifel/git-auto-commit-action@v1.0.0"
  secrets = ["GITHUB_TOKEN"]
  env = {
    COMMIT_MESSAGE = "Apply php-cs-fixer changes"
    COMMIT_AUTHOR_EMAIL  = "john.doe@example.com"
    COMMIT_AUTHOR_NAME = "John Doe"
  }
}

Now on every push, possible style changes are automatically committed and pushed back to your repository. No need to run the command manually or for a Third-Party-service.

Screenshot of auto-commit. Two committers have been attributed with the commit.
Screenshot of auto-commit. Two committers have been attributed with the commit.

We now also added our first secret: GITHUB_TOKEN. This is a special secret which is available to all Actions in a repository. But it's not enabled by default. You have to add it manually in the visual editor on github.com

Add the GITHUB_TOKEN by checking the corresponding checkbox in the editor.
Add the GITHUB_TOKEN by checking the corresponding checkbox in the editor.

As this Action uses the GITHUB_TOKEN to authenticate the git push-command, GitHub won't trigger a second run of the Workflow. (Keep that in mind!)

I think this covers the basics of GitHub Actions for PHP developers. I'm very excited about Actions and what the future holds. I hope the feature leaves the beta soon, so that more people can use it in their projects.

GitHub already announced support for Scheduled Workflows which opens a big realm of possibilities. (Jason Etcovitch writes here how he uses Scheduled Workflows to automatically create weekly meeting notes). Or for example, you could also build an Uptime-Monitoring Action which is triggered every few minutes and would send Slack or Email notifications.

Personally, I would like to use the Scheduling feature to fully automate Laravel Download Statistics (a side project of mine). A workflow could trigger the update of download numbers, create a new HTML export and push everything to Netlify.
No need for humans any more 🤖🤯.

If I could excite you about Actions and you want to start developing your own Actions, here are a few resources I've found while working on this article:

And here's a list of Actions I found, which I actually would like to implement in my projects.

Thanks to Peter Brinck, Célien Boillat and Max Almonte for proof reading this article and giving feedback.