How to build GitHub Actions in PHP with Minicli and Docker

How to build GitHub Actions in PHP with Minicli and Docker

Posted 2 weeks ago in

GitHub Actions facilitates creating CI/CD automated workflows that can be triggered by GitHub events, such as when a pull request is created, a merge is made, or a new comment is posted on an issue. What some people may not know is that you can also run GitHub actions at scheduled times, based on cron expressions.

Public repositories get unlimited GitHub Actions for free, which makes this feature a really great tool for open source and personal projects that must run scheduled tasks.

In this guide, you'll learn how to use GitHub Actions to periodically run a PHP command line application built with Minicli. This application will update a CONTRIBUTORS file in the same repository where the workflow action is set, updating information about top contributors of a project. We'll implement the repository update portion using the update-files-on-github action, which will generate a commit with the file change to the repository where the workflow is run.

Pull request automatically created by this workflow + action

Prerequisites

To follow this tutorial you'll need access to:

  • A PHP command line environment (php-cli, no need for web servers) and Composer installed. The php-curl extension is required to connect to the GitHub API.
  • An empty GitHub repository where you're going to set up your action. For more info, check this documentation.

Step 1: Bootstrapping the application

Start by bootstrapping a new Minicli application. This will be a single-command application, so we don't need to use the minicli/application template. Create a new folder and require minicli/minicli to start building your command:

cd ~
mkdir action-contributors
cd action-contributors
composer require minicli/minicli

This will create a new composer.json file and download the base minicli/minicli package. Next, you need to create the entry point script that runs your command. Using your code editor of choice, create a new file in the root of the application called minicli (or another name of your choice):

nano minicli

Include the following code, which bootstraps a Minicli application with a single command defined as callback:

#!/usr/bin/php
<?php

if(php_sapi_name() !== 'cli') {
    exit;
}

require __DIR__ . '/vendor/autoload.php';

use Minicli\App;

$app = new App([
    'app_path' => __DIR__ . '/app/Command'
]);

$app->registerCommand('update-contributors', function () use ($app) {
    $app->getPrinter()->info('Fetching top contributors...');
});

$app->runCommand($argv);

Save and close the file. Then, run the following command to make this script executable:

chmod+x minicli

Now you can test your command with:

./minicli update-contributors

screenshot showing the output from the command in the terminal

In the next step, you'll update the example command to pull the top contributors of a GitHub project, and generate a markdown file with the list.

Step 2: Pulling contributors with the GitHub API

To make requests to the GitHub API, we'll use the Curly Minicli extension. You can import it to your project with the following command:

composer require minicli/curly

Obtaining the top contributors for an open source project on GitHub doesn't require an authentication token. You only need to include a couple headers in your request:

  • Accept: application/vnd.github.v3+json
  • User-Agent: My user agent v1.0

You'll now edit the update-contributors command to query the GitHub API and save information about top contributors in the project of your choice.

Replace the current content in your minicli script with the following, updated code:

#!/usr/bin/php
<?php

if(php_sapi_name() !== 'cli') {
    exit;
}

require __DIR__ . '/vendor/autoload.php';

use Minicli\App;
use Minicli\Curly\Client;

$app = new App([
    'app_path' => __DIR__ . '/app/Command',
    'repository' => getenv('CONTRIB_REPOSITORY') ?: 'minicli/minicli',
    'output_file' => getenv('CONTRIB_OUTPUT_FILE') ?: 'CONTRIBUTORS.md'
]);

$app->registerCommand('update-contributors', function () use ($app) {
    $app->getPrinter()->info('Fetching top contributors...');

    $client = new Client();
    $response = $client->get(
                "https://api.github.com/repos/" . $app->config->repository. "/contributors",
                ['Accept: application/vnd.github.v3+json', 'User-Agent: Curly']
    );

    if ($response['code'] != 200) {
            $app->getPrinter()->error("an error occurred: " . $response['code']);
            return 1;
    }

    $content = "#Contributors\n\n";
    $content .= "Shout out to our top contributors!\n\n";

    foreach (json_decode($response['body']) as $item) {
        $content .= "- [$item->login]($item->url)\n";
    }

    try {
            $contrib_file = fopen($app->config->output_file, 'w+');
            fwrite($contrib_file, $content);
            fclose($contrib_file);
    } catch (Exception $exception) {
            $app->getPrinter()->error("An error occurred while trying to save the contrib file.");
            return 1;
    }

    $app->getPrinter()->success("Finished updating contrib file.");
    return 0;
});

$app->runCommand($argv);

The updated code defines a couple configuration variables: repository and output_file, with default values set to minicli/minicli and CONTRIBUTORS.md respectively. When building your action workflow, you can overwrite these default values with environment variables named CONTRIB_REPOSITORY and CONTRIB_OUTPUT_FILE, respectively.

The update-contributors method now uses a Curly/Client client to query the GitHub API using the endpoint https://api.github.com/repos/owner/repo/contributors. When the request is successful, the application builds a markdown text with the contributors that are returned as response, and writes it to the file defined by the output_file configuration value.

Now run the application again with:

./minicli update-contributors
Fetching top contributors...

Finished updating contrib file.

If you check your repository files now, you should see a new CONTRIBUTORS.md file in the root of the repository.

cat CONTRIBTORS.md
# Contributors

Shout out to our top contributors!

- [erikaheidi](https://api.github.com/users/erikaheidi)
- [syntaxseed](https://api.github.com/users/syntaxseed)
- [tombenevides](https://api.github.com/users/tombenevides)
- [ScullWM](https://api.github.com/users/ScullWM)
- [wandersonwhcr](https://api.github.com/users/wandersonwhcr)
- [lotfio](https://api.github.com/users/lotfio)
- [flug](https://api.github.com/users/flug)
- [mauriciofauth](https://api.github.com/users/mauriciofauth)
- [mrpc](https://api.github.com/users/mrpc)
- [peter279k](https://api.github.com/users/peter279k)
- [zaghadon](https://api.github.com/users/zaghadon)

The application is now ready, but you still need to set up the environment that will execute it on the GitHub runtime.

In the next step, you'll build a custom Docker image based on PHP 8.0 to install and execute the application.

Step 3: Setting up the application Dockerfile

GitHub offers a few different runners to execute code as actions. For PHP applications, you'll need to provide the runner with a custom environment based on a Docker image, capable of executing your application code as a single command.

Create a new Dockerfile in the root of your application and copy the following code to the file:

FROM php:8.0-cli

RUN apt-get update && apt-get install -y \
    git \
    curl \
    libxml2-dev \
    zip \
    unzip

RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# Install Composer and set up application
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN mkdir /application
COPY . /application/
RUN cd /application && composer install

ENTRYPOINT [ "php", "/application/minicli" ]
CMD ["update-contributors"]

Save the file.

The example Dockerfile starts by setting up the base image to PHP 8.0-cli. Then, it will:

  • set up a few required packages;
  • install Composer by copying its executable from its official image;
  • create a directory for the application inside the container;
  • copy the application files from the current directory and into the container;
  • run composer install;
  • set up the container entry point and default command.

You may want to test if the application runs through Docker with this image. Use the following command to build the image under the tag action-contributors:

docker build . -t action-contributors

To run the application in a disposable container using the newly built image, run:

docker container run --rm -v $(pwd) action-contributors

You should be able to see the same output as before. However, the generated CONTRIBUTORS.md file will be confined to the container and won't show up in your application directory on the host machine. When setting up your workflow, you'll need to include an additional GitHub Action to either commit the changes directly to the master branch, or open a pull request with the changes so that you can review the update before merging.

Step 4: Creating the action file

With the application ready, you'll need to set up a YAML file to define your action. Create a new file called action.yml on the root of the project, and copy the following content to that file:

# action.yml
name: 'Update CONTRIBUTORS'
description: 'Updates contributors file'
outputs:
  response:
    description: 'Output from command'
runs:
  using: 'docker'
  image: 'Dockerfile'

Save the file.

The file starts by defining the name and description of the action. Because our command doesn't require inputs, we don't need to set up an inputs section for this action. The response output will be available for logs that might refer to this information for debug purposes. Then, we get to the runs portion, where we define what the action will do. This action will build and execute the image defined by Dockerfile.

Your action is almost ready. You're encouraged to create a README.md file explaining how to use it, and including an example workflow. For now, you can create a simple README with some basic information about the action. You can use the following template for your README:

# My Action Title

A paragraph about my action, what it does and how it works.


## Example usage
Include an example of workflow using this action.

When you're finished with your README, you'll need to commit and push the files to the GitHub repository you've created:

git add action.yml composer.json composer.lock minicli Dockerfile README.md
git commit -m "My first action is ready"
git tag -a -m "My first action release" v1
git push --follow-tags

Once you have pushed your code (including the tag), the action is ready to be used by a workflow in any project on GitHub, referenced by your_user_or_org/your_action_repo@v1.

In the next step, you'll create a workflow to test this action.

Step 5: Setting up a workflow

When creating a workflow for a GitHub action, there are quite a few different things to consider: what triggers the action, which other actions are needed and in which order they should run, and what kinds of inputs or environment variables are required for the action to run.

This action should run on a scheduled basis, without the need for a specific event to trigger it. It doesn't require inputs, but it uses two environment variables to define which repository is being pulled for contributors, and the name of the file that will be created with the list of contributors.

We'll need to combine this action with another action to either commit the changes directly to the main project's branch, or open a pull request with the changes.

Committing the updated CONTRIBUTORS file directly to the main branch

The following workflow will run once a month and commit an updated CONTRIBUTORS.md file directly into the default remote branch of the project where this workflow is set:

name: Update CONTRIBUTORS file
on:
  schedule:
    - cron: "0 0 1 * *"
  workflow_dispatch:
jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: minicli/action-contributors@v3
        name: "Update a projects CONTRIBUTORS file"
        env:
          CONTRIB_REPOSITORY: 'minicli/minicli'
          CONTRIB_OUTPUT_FILE: 'CONTRIBUTORS.md'
      - name: Commit changes
        uses: test-room-7/action-update-file@v1
        with:
          file-path: 'CONTRIBUTORS.md'
          commit-msg: Update Contributors
          github-token: ${{ secrets.GITHUB_TOKEN }}

Remember to change the CONTRIB_REPOSITORY environment variable to the project you want to pull contributors from, using the format owner/repository.

Opening a Pull Request with the updated CONTRIBUTORS file

You can also opt to open a pull request instead of committing the changes directly into the main project's branch. For that, you'll need an additional action called actions/checkout. This action checks out the repository code to a location inside the container.

name: Update CONTRIBUTORS file
on:
  schedule:
    - cron: "0 0 1 * *"
  workflow_dispatch:
jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: minicli/action-contributors@v3
        name: "Update a projects CONTRIBUTORS file"
        env:
          CONTRIB_REPOSITORY: 'minicli/docs'
          CONTRIB_OUTPUT_FILE: 'CONTRIBUTORS.md'
      - name: Create a PR
        uses: peter-evans/create-pull-request@v3
        with:
          commit-message: Update Contributors
          title: "[automated] Update Contributors File"
          token: ${{ secrets.GITHUB_TOKEN }}

Copy your preferred workflow code to the following location, inside the project where you want the keep your CONTRIBUTORS file:

.github/workflows/contributors.yml

Then, commit and push your changes to the repository where the workflow will run. Once you push the workflow file to that location, you'll be able to see the workflow listed in the Actions tab of your GitHub repository:

Running a Github action manually

Then, you can manually run the workflow by clicking on the Run workflow button on the right.

If you choose to commit the changes directly into the main project's branch, once the workflow has finished running you should find the updated CONTRIBUTORS file in the root of the project.

If you chose to create a pull request, you should find an open pull request in the "Pull requests" tab of your project's repository, carrying the updated CONTRIBUTORS file.

Pull request automatically created by this workflow + action

Find more example workflows using these actions

GitHub provides a large library of readily available actions you can integrate into your project, and you can also find user-contributed actions in the marketplace. However, because workflows are so flexible, sometimes it can be difficult to figure out how to combine multiple actions and which inputs or environment variables are required in certain scenarios.

To find usage examples related to the actions used in this guide, you can use the following Sourcegraph search queries:

###Find more examples of the actions/checkout action

Search Query:

lang:YAML uses: actions/checkout@v2

Search URL: https://sourcegraph.com/search?q=context:global+lang:YAML+uses:+actions/checkout%40v2&patternType=literal

Find more examples of the test-room-7/action-update-file action

Search Query:

lang:YAML uses: test-room-7/action-update-file@v1

Search URL: https://sourcegraph.com/search?q=context:global+lang:YAML+uses:+test-room-7/action-update-file%40v1&patternType=literal

Find more examples of the peter-evans/create-pull-request action

Search Query:

lang:YAML uses: peter-evans/create-pull-request@v3

Search URL: https://sourcegraph.com/search?q=context:global+lang:YAML+uses:+peter-evans/create-pull-request%40v3&patternType=literal

Search for anything related to GitHub actions

Search URL https://sourcegraph.com/search?q=context:global+github+actions&patternType=literal

Search Query:

github actions

Conclusion

In this step-by-step tutorial, we've seen how to create a GitHub action to programmatically update a CONTRIBUTORS file for an open source project, using the Minicli framework for command line applications in PHP. If you'd like to use this action in one of your projects and don't want to build it from scratch, you can head over to the minicli/action-contributors repository on GitHub to set up this action within a workflow on your project. To find more about GitHub actions, you can check the official documentation, and you can also search for practical examples of workflows using Sourcegraph code search.