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.
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. Thephp-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
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:
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.
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
Find more examples of the peter-evans/create-pull-request
action
Search Query:
lang:YAML uses: peter-evans/create-pull-request@v3
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.
Top comments (2)
If you anticipate developing other actions with php, you might consider building a base image with all of the common stuff you'd need preinstalled, like git, curl, etc. The dockerfile for each action would then just need to add the action's code and set the entrypoint. An added benefit would be faster loading of the action since you wouldn't be installing stuff each time it runs.
For example, I maintain several GitHub Actions that are implemented in Python. I have a base Docker image that is a python image with git, curl, the GitHub CLI, and some other stuff preinstalled. So the Dockerfile of each action just needs to add the action's source.
Here's the repo to pyaction:
cicirello / pyaction
A base Docker image for Github Actions implemented in Python
pyaction
A base Docker image for Github Actions implemented in Python
Summary
This Docker image is designed to support implementing Github Actions with Python. As of version 4.0.0., it starts with the official python docker image as the base which is a Debian OS. It specifically uses python:3-slim to keep the image size down for faster loading of Github Actions that use pyaction. On top of the base, we've installed curl gpg, git, and the GitHub CLI. We added curl and gpg because they are needed to install the GitHub CLI, and they may come in handy anyway (especially curl) when implementing a GitHub Action.
Note: Up through pyaction:3.14.0, we previously used Alpine Linux. However the GitHub CLI isn't currently supported on Alpine, which is why we have switched the base image.
Multiplatform Image
Version 4.0.0 and Newer: pyaction supports the following platforms:
It can be pulled from either Docker Hub or the GitHub Container Registry, probably faster from the latter when used in GitHub Actions.
And here is an action that uses it:
cicirello / count-action-users
Generates Shields endpoint with number of users of a GitHub Action
count-action-users
Check out all of our GitHub Actions: actions.cicirello.org/
About
The cicirello/count-action-users GitHub Action generates a Shields endpoint with the count of the number of workflows that use a GitHub Action. It is thus a tool for maintainers of GitHub Actions, and it can be used to insert a badge with a users count into the README for a GitHub Action. The key features include:
count-action-users
action accepts a list of GitHub Actions as an input, generating endpoints for all…This is really great, thank you for sharing!