GitHub Workflows and GitHub Actions
The service called GitHub Actions provided by GitHub (Microsoft) allows us to run arbitrary workflows on their ephemeral (temporay) servers.
CI: Often this workflow is the CI (Continuous Integration) of a project, compilingt the source code, setting ups external services, running unit- and integrations tests.
Data collection: In other cases this workflow is scheduled job collecting data and building a web site.
The service has a limited free tier for public repositories and you can buy minues for extensive running and for private repositories.
The name GitHub Actions is also used for some pre-defined building blocks one can use to create a Workflow.
A little background
A little background about git, GitHub, CI (Continuous Integration) and CD (Continuous Deivery).
What is Git
- Distributed Version Control System
What is GitHub
- Cloud-based hosting for git repositories
- Now owned by Microsoft
CI - Continuous Integration
- Make sure all the code works together.
- Run as frequently as possible.
When to run?
- “Nightly build”
- …
- On every commit (every push).
Also
- On each pull-request
- Scheduled to ensure the code does not break while the dependencies might.
- Manually (e.g. to show during presentations or to run with specific flags.)
- Via API call (e.g. to let one job trigger another one.)
What to run?
- Compilation
- Unit tests
- Integration tests
- Acceptances tests
- …
- Whatever you can
CD - Continuous Delivery (or Deployment)
- After all the tests are successful and maybe when some special condition is met (e.g. a tag was added), automatically create a release and deploy the code.
Cloud-based CI systems
Exercises
-
Fork the repository so you have your own copy
-
Clone the forked repo
-
Enable Travis, Appveyor
-
Add tests
-
Add badges to the
README.mdof the repo. -
Add test coverage reporting
GitHub Actions
What are GitHub Actions?
- GitHub Actions is (are?) a service provided by GitHub to run any process triggered by some event. (e.g. push, pull-request, manually, scheduled, …)
- A popular use is to setup Continuous Integration (CI) and Continuous Delivery (CD) system.
- Free for limited use for both public and private repositories.
- You can check the pricing for details.
- Check Total time used
- See the billing
GitHub Actions use-cases
- CI - Continuous Integration - compile code and run all the tests on every push and ever pull-request.
- Run both unit-tests and integration tests.
- Run linters and other static analyzers.
- CD - Continuous Delivery/Deployment.
- Automatically handle issues (eg. close old issues).
- Setup environment for GitHub Co-Pilot.
- Run a scheduled job to collect data.
- Generate static web sites.
Documentation
There is extensive Documentation of GitHub Actions. You can even open issues and send pull-requests to update the documentation, but that won’t always succeed.
Setup GitHub Actions via the UI GitHub
- GitHub
- Create a new repository.
- There is a link called “Actions” at the top of the page. Click on it.
- There are many ready-made workflows one can get started with in a few clicks.
- If you already have a project on GitHub then it is enough to go to the “Actions” tab.
- Pick one of the suggested workflows and let GitHub create a file for you in the
.github/workflowsfolder.
Setup GitHub Actions manually
You can do this via the web interface of GitHub or you can clone the project locally, do it there and then push the changes to GitHub.
- Create a folder in the root of your repository called
.github/workflows. - Create a YAML file in it.
- The name of the file does not matter.
- The extension must be either
.yamlor.yml. - I often call it
.github/workflows/ci.yaml. - For content see bellow.
- Commit the changes.
- If you worked locally on your computer then push the changes to GitHub.
- Once you did this visit the
Actionstab of your repository on GitHub. After a few seconds you should see the indication that a job is running and then after a while you will see the results.
# We need a trigger
on:
# This will run the workflow on every push
push:
# One trigger is enough.
# We added this so we can run the workflow manually as well without making changes.
workflow_dispatch:
# We need at least on job
jobs:
# The name of the job can be any arbitrary name. In this it will be "ci"
ci:
# We have to state which platform to run on:
runs-on: ubuntu-latest
# We need one or more steps:
steps:
# The first and only step is executing a shell command to print Hello World
- run: echo Hello World
# We don't need any of the comments...
- Next we’ll see examples for these YAML files.
Minimal Ubuntu Linux configuration
# Use some descriptive name
name: Minimal Ubuntu Workflow
# The events that trigger this workflow.
on:
push:
# We added this so we can run the workflow manually as well without making changes.
workflow_dispatch:
# One or more jobs to run.
jobs:
# `build`, the name of the job, is arbitrary, you can use any word for the name of your jobs.
build:
# runs-on: the platform (operating system) the job runs on. (e.g. Ubuntu, Windows, Mac)
runs-on: ubuntu-latest
# steps - Each step can have a name and it must have a run.
steps:
# name - optional, just a description
- name: Single step
# run - One or more commands executed in a shell.
run: echo Hello World
- name: Look around
# When excuting more command we can use a pipeline | to indicate this.
# The goal of the following commands is to show you what is included in the
# ubuntu-latest platform.
run: |
uname -a
pwd
whoami
uptime
which perl
# On Linux python is Python 2 and python3 is Python 3
which python
which python3
which ruby
which node
which java
perl -v
python -V
python3 -V
ruby -v
node -v
javac -version
java -version
Minimal Windows configuration
A sample configuration that runs on Windows (as per the runs-on field).
The main difference is the type of commands that one can run on Windows vs. what can run on Linux on macOS.
# Use some descriptive name
name: Minimal Windows Workflow
# The events that trigger this workflow.
on:
push:
# We added this so we can run the workflow manually as well without making changes.
workflow_dispatch:
# One or more jobs to run.
jobs:
# `build`, the name of the job, is arbitrary, you can use any word for the name of your jobs.
build:
# runs-on: the platform (operating system) the job runs on. (e.g. Ubuntu, Windows, Mac)
runs-on: windows-latest
# steps - Each step can have a name and it must have a run.
steps:
# name - optional, just a description
- name: Single step
# run - One or more commands executed in a shell.
run: echo Hello World
- name: Look around
# When excuting more command we can use a pipeline | to indicate this.
# The goal of the following commands is to show you what is included in the
# windows-latest platform.
run: |
uname -a
pwd
whoami
uptime
which perl
which python
#which python3
which ruby
which node
which java
perl -v
# On Windows python is Python 3 and python3 does not exist.
python -V
#python3 -V
ruby -v
node -v
javac -version
java -version
Minimal MacOS configuration
A sample configuration that runs on macOS (as per the runs-on field).
# Use some descriptive name
name: Minimal macOS Workflow
# The events that trigger this workflow.
on:
push:
# We added this so we can run the workflow manually as well without making changes.
workflow_dispatch:
# One or more jobs to run.
jobs:
# `build`, the name of the job, is arbitrary, you can use any word for the name of your jobs.
build:
# runs-on: the platform (operating system) the job runs on. (e.g. Ubuntu, Windows, Mac)
runs-on: macos-latest
# steps - Each step can have a name and it must have a run.
steps:
# name - optional, just a description
- name: Single step
# run - One or more commands executed in a shell.
run: echo Hello World
- name: Look around
# When excuting more command we can use a pipeline | to indicate this.
# The goal of the following commands is to show you what is included in the
# macos-latest platform.
run: |
uname -a
pwd
whoami
uptime
which perl
which python
which python3
which ruby
which node
which java
perl -v
python -V
python3 -V
ruby -v
node -v
javac -version
java -version
Minimal Docker configuration (for python)
# Use some descriptive name
name: Minimal workflow for Python using Docker image
# The events that trigger this workflow.
on:
push:
# We added this so we can run the workflow manually as well without making changes.
workflow_dispatch:
# One or more jobs to run.
jobs:
# `build`, the name of the job, is arbitrary, you can use any word for the name of your jobs.
build:
# runs-on: the platform (operating system) the job runs on.
runs-on: ubuntu-latest
# The docker image, by default from https://hub.docker.com/ to use.
container:
image: python:3.14
# steps - Each step can have a name and it must have a run.
steps:
- name: Look around
# When excuting more command we can use a pipeline | to indicate this.
# The goal of the following commands is to show you what is included in the
# specific Docker image.
run: |
uname -a
python -V
Name of a workflow
- The
nameis just some free text to help identify the workflow.
name: Free Text defaults to the filename
Triggering jobs
- The required
onkeyword describes the events that trigger workflows.
Single event
If the workflow is triggered by a single event then you can write it like this:
on: push
Multiple events
If you’d like to configure multiple events, you have several ways to do that.
on: [push, pull_request, workflow_dispatch]
You can also write:
on:
push:
pull_request:
workflow_dispatch:
- Run on “push” in every branch.
- Run on “pull_request” if it was sent to the “dev” branch.
workflow_dispatchto run manually via the web site of GitHub.- Scheduled every 5 minutes (cron config)
name: Triggers
on:
push:
branches: '*'
pull_request:
branches: 'dev'
workflow_dispatch:
schedule:
- cron: '*/5 * * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Look around
run: |
echo $GITHUB_EVENT_NAME
printenv | sort
- Manual events (via POST request)
Environment variables
-
env
-
GITHUB_*are reserved.
env:
DEMO_FIELD: value
name: Environment Variables
on:
push:
workflow_dispatch:
# We can set an environment variable for
# - the whole file (all the jobs)
# - for a job
# - for a single step
env:
DEMO: "File level"
jobs:
global:
runs-on: ubuntu-latest
steps:
- name: Show the environment variable we set for the file
run: |
echo $DEMO
- name: Show the environment variable we set inside this step
env:
DEMO: "Step level"
run: |
echo $DEMO
other:
runs-on: ubuntu-latest
env:
DEMO: "Job level"
steps:
- name: Show job level variable
run: |
echo $DEMO
- name: Show step level variable
env:
DEMO: "Step level"
run: |
echo $DEMO
all:
runs-on: ubuntu-latest
steps:
- name: Show all the environment variables set by GitHub Actions
run: printenv | sort
GitHub Action Parallel Jobs
- Jobs run parallel by default
name: Parallel running
on:
push:
workflow_dispatch:
jobs:
test_a:
runs-on: ubuntu-latest
steps:
- name: Step A1
run: |
echo start
sleep 10
echo end
- name: Step A2
run: |
echo start
sleep 10
echo end
test_b:
runs-on: ubuntu-latest
steps:
- name: Step B1
run: |
echo start
sleep 10
echo end
- name: Step B2
run: |
echo start
sleep 10
echo end
test_c:
runs-on: ubuntu-latest
steps:
- name: Step C1
run: |
echo start
sleep 10
echo end
- name: Step C2
run: |
echo start
sleep 10
echo end
test_d:
runs-on: ubuntu-latest
steps:
- name: Step D1
run: |
echo start
sleep 10
echo end
- name: Step D2
run: |
echo start
sleep 10
echo end
name: Parallel running
on:
push:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
version: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
name: ${{ matrix.version }}
steps:
- name: Step 1
run: |
echo start
sleep 30
echo end
GitHub Actions - Runners - runs-on
-
runs-on
-
Linux, Windows, or MacOS running on Azure
Scheduled runs
name: Push and schedule
on:
push:
branches: '*'
pull_request:
branches: '*'
schedule:
- cron: '*/5 * * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Single step
run: |
echo Hello World
echo $GITHUB_EVENT_NAME
- name: Look around
run: |
printenv | sort
- name: Conditional step (push)
if: ${{ github.event_name == 'push' }}
run: |
echo "Only run on a push"
- name: Conditional step (schedule)
if: ${{ github.event_name == 'schedule' }}
run: |
echo "Only run in schedule"
- name: Conditional step (pull_request)
if: ${{ github.event_name == 'pull_request' }}
run: |
echo "Only run in pull-request"
- name: Step after
run: |
echo "Always run"
Conditional runs
name: Push and schedule
on:
push:
branches: '*'
pull_request:
branches: '*'
schedule:
- cron: '*/5 * * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Single step
run: |
echo Hello World
echo $GITHUB_EVENT_NAME
- name: Look around
run: |
printenv | sort
- name: Conditional step (push)
if: ${{ github.event_name == 'push' }}
run: |
echo "Only run on a push"
- name: Conditional step (schedule)
if: ${{ github.event_name == 'schedule' }}
run: |
echo "Only run in schedule"
- name: Conditional step (pull_request)
if: ${{ github.event_name == 'pull_request' }}
run: |
echo "Only run in pull-request"
- name: Step after
run: |
echo "Always run"
Disable GitHub Action workflow
- In the Settings/Actions of your repository you can enable/disable “Actions”
Disable a single GitHub Action job
- Adding a if-statement that evaluates to false to the job
- See literals
jobs:
job_name:
if: ${{ false }} # disable for now
Disable a single step in a GitHub Action job
- Adding an if-statement that evaluates to false to the step:
jobs:
JOB_NAME:
# ...
steps:
- name: SOME NAME
if: ${{ false }}
Create multiline file in GitHub Action
In this workflow example you can see several ways to creta a file from a GitHub Action workflow.
I am not sure if doing so is a good practice or not, I’d probbaly have a file someone in the repository and a script that will copy it, if necessary. Then I’d call that script in my YAML file.
name: Create file
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Create file
run: |
printf "Hello\nWorld\n" > hw.txt
- name: Create file
run: |
echo First > other.txt
echo Second Line >> other.txt
echo Third >> other.txt
- name: Show file content
run: |
pwd
ls -la
cat hw.txt
cat other.txt
- name: Create directory and create file in homedir
run: |
ls -la ~/
mkdir ~/.zorg
echo First > ~/.zorg/home.txt
echo Second Line >> ~/.zorg/home.txt
echo Third >> ~/.zorg/home.txt
ls -la ~/.zorg/
- name: Show file content
run: |
ls -la ~/
cat ~/.zorg/home.txt
OS Matrix (Windows, Linux, Mac OSX)
name: OS Matrix
on: push
jobs:
build:
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.runner}}
steps:
- uses: actions/checkout@v6
- name: View environment
run: |
uname -a
printenv | sort
Matrix (env vars)
-
matrix
-
strategy
-
fail-fast
name: Matrix environment variables
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
fruit:
- Apple
- Banana
meal:
- Breakfast
- Lunch
steps:
- name: Single step
env:
DEMO_FRUIT: ${{ matrix.fruit }}
DEMO_MEAL: ${{ matrix.meal }}
run: |
echo $DEMO_FRUIT for $DEMO_MEAL
-
Create a matrix of configuration options to run the jobs. (e.g. on different operating systesm, different versions of the compiler, etc.)
-
fail-fast: What should happen when one of the cases fails? Should all run to completion or should we stop all the jobs if one already failed?
Change directory in GitHub Actions
name: cd
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Experiment
run: |
pwd
# /home/runner/work/github-actions-change-directory/github-actions-change-directory
mkdir hello
cd hello
pwd
# /home/runner/work/github-actions-change-directory/github-actions-change-directory/hello
- name: In another step
run: |
pwd
# /home/runner/work/github-actions-change-directory/github-actions-change-directory
Install packages on Ubuntu Linux in GitHub Actions
name: Install Linux packages
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Before
run: |
which black || echo black is missing
- name: Install package
run: |
sudo apt-get -y install black
- name: Try black
run: black .
# Use `black --check .` to verify the formatting and make the CI fail if it needs updating.needs updating.
- name: diff
run: git diff
Generate GitHub pages using GitHub Actions
name: Generate web page
on:
push:
branches: '*'
schedule:
- cron: '*/5 * * * *'
# page_build:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Environment
run: |
printenv | grep GITHUB | sort
- name: Create page
run: |
mkdir -p docs
date >> docs/dates.txt
echo '<pre>' > docs/index.html
sort -r docs/dates.txt >> docs/index.html
echo '</pre>' >> docs/index.html
- name: Commit new page
if: github.repository == 'szabgab/try'
run: |
GIT_STATUS=$(git status --porcelain)
echo $GIT_STATUS
git config --global user.name 'Gabor Szabo'
git config --global user.email 'szabgab@users.noreply.github.com'
git add docs/
if [ "$GIT_STATUS" != "" ]; then git commit -m "Automated Web page generation"; fi
if [ "$GIT_STATUS" != "" ]; then git push; fi
Workflow Dispatch (manually and via REST call)
name: Push and Workflow Dispatch
on:
push:
branches: '*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Single step
run: |
printenv | grep GITHUB | sort
Run in case of failure
name: Failure?
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
build:
runs-on: ubuntu-latest
name: Job
steps:
- uses: actions/checkout@v6
- name: Step one
run: |
echo Always runs
#ls blabla
- name: Step two (run on failure)
if: ${{ failure() }}
run: echo There was a failure
- name: Step three
run: |
echo Runs if there was no failure
#ls blabla
You can create a step that will run only if any of the previous steps failed. In this example if you enable the “ls” statement in “Step one” it will fail that will make Step two execute, but Step three won’t because there was a failure.
On the other hand if Step One passes then Step Two won’t run. Step three will then run.
A failure in step three (e.g. by enabling the ls statement) will not make step Two run.
Setup Droplet for demo
apt-get update
apt-get install -y python3-pip python3-virtualenv
pip3 install flask
copy the webhook file
FLASK_APP=webhook flask run --host 0.0.0.0 --port 80
Integrated feature branches
-
Commit back (See Generate GitHub Pages)
-
Don’t allow direct commit to “prod”
-
Every push to a branch called /release/something will check if merging to “prod” would be a fast forward, runs all the tests, merges to “prod” starts deployment.
Deploy using Git commit webhooks
- Go to GitHub repository
- Settings
- Webhooks
Payload URL: https://deploy.hostlocal.com/
Content tpe: application/json
Secret: Your secret
from flask import Flask, request
import logging
import hashlib
import hmac
app = Flask(__name__)
app.logger.setLevel(logging.INFO)
@app.route("/")
def main():
return "Deployer Demo"
@app.route("/github", methods=["POST"])
def github():
data_as_json = request.data
#app.logger.info(request.data_as_json)
signature = request.environ.get('HTTP_X_HUB_SIGNATURE_256', '')
app.logger.info(signature) # sha256=e61920df1d6fb1b30319eca3f5e0d0f826b486a406eb16e46071c6cdd0ce3f9b
GITHUB_SECRET = 'shibboleth'
expected_signature = 'sha256=' + hmac.new(GITHUB_SECRET.encode('utf8'), data_as_json, hashlib.sha256).hexdigest()
app.logger.info(expected_signature)
if signature != expected_signature:
app.logger.info('invalid signature')
return "done"
app.logger.info('verified')
data = request.json
#app.logger.info(data)
app.logger.info(data['repository']['full_name'])
app.logger.info(data['after'])
app.logger.info(data['ref']) # get the branch
# arbitrary code
return "ok"
@app.route("/action", methods=["POST"])
def action():
app.logger.info("action")
secret = request.form.get('secret')
#app.logger.info(secret)
GITHUB_ACTION_SECRET = 'HushHush'
if secret != GITHUB_ACTION_SECRET:
app.logger.info('invalid secret provided')
return "done"
app.logger.info('verified')
sha = request.form.get('GITHUB_SHA')
app.logger.info(sha)
repository = request.form.get('GITHUB_REPOSITORY')
app.logger.info(repository)
# arbitrary code
return "ok"
Deploy from GitHub Action
-
Go to GitHub repository
-
Settings
-
Environments
-
New Environment
-
Name: DEPLOYMENT
-
Add Secret:
-
Name: DEPLOY_SECRET
-
Value: HushHush
-
curl from GitHub action
-
we need to send a secret, a repo name, and a sha
Deploy using ssh
ssh-keygen -t rsa -b 4096 -C "user@host" -q -N "" -f ./do
ssh-copy-id -i do.pub user@host
- Add Secret:
- Name: PRIVATE_KEY
- Value: … the content of the ‘do’ file …
ssh-keyscan host
- Add Secret:
- Name: KNOWN_HOSTS
- Value: … the output of the keyscan command …
Artifact
- In the first job we create a file called date.txt and save it as an artifact.
- Then we run 3 parallel jobs on 3 operating systems where we dowload the artifact and show its content.
name: OS and Perl Matrix
on: push
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v6
- name: View environment
run: |
uname -a
printenv | sort
- name: Build
run: |
date > date.txt
cat date.txt
- name: Archive production artifacts
uses: actions/upload-artifact@v2
with:
name: the-date
path: |
date.txt
test:
needs: build
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.runner}}
name: OS ${{matrix.runner}}
steps:
- name: View environment
if: ${{ ! startsWith( matrix.runner, 'windows-' ) }}
run: |
uname -a
printenv | sort
ls -l
- name: Download a single artifact
uses: actions/download-artifact@v2
with:
name: the-date
- name: View artifact on Linux and OSX
if: ${{ ! startsWith( matrix.runner, 'windows-' ) }}
run: |
ls -l
cat date.txt
date
- name: View artifact on Windows
if: ${{ startsWith( matrix.runner, 'windows-' ) }}
run: |
dir
type date.txt
date
Lock Threads
-
Automatically lock closed Github Issues and Pull-Requests after a period of inactivity.
name: 'Lock Threads'
on:
schedule:
- cron: '0 0 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '14'
GitHub Workflows
-
github-workflows - simple reusable workflow
-
Example:
-
the workflow in osdc-2023-01-perl uses osdc-site-generator
List of files changed
name: Using Action
# Demonstrate how to list the files that were changed during the most recent push.
# Which can contain 1 or more commits.
# * Manually - it is rather problematic.
# * Using the [changed-files](https://github.com/marketplace/actions/changed-files) action.
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v6
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v47
# NOTE: `since_last_remote_commit: true` is implied by default and falls back to the previous local commit.
- name: List all changed files
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
run: |
echo "$ALL_CHANGED_FILES"
echo "------"
for file in ${ALL_CHANGED_FILES}; do
echo "$file was changed"
done
name: Manually
on:
push:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
name: Manual
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 6
- name: Changed files
run: |
echo 2
git diff --name-only HEAD~2
echo 5
git diff --name-only HEAD~5
# This will fail because not enough commits
#echo 6
#git diff --name-only HEAD~6
Avoid duplicate triggers
- Avoid duplicate triggers (push and pull_request for maintainers)
Needs - GitHub jobs and workflows depending on each other
name: CI
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '42 5 * * *'
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- name: Build step
run: |
echo This is the placeholder for the compilation step
echo "Enable the next line if you'd like to see what happens when the job fails."
# ls -l something_that_does_not_exist
test:
runs-on: ubuntu-latest
name: Test
needs: [build]
steps:
- name: Test step
run: |
echo This job only runs if the build job was successful
linter:
runs-on: ubuntu-latest
name: Linter
steps:
- name: Linter step
run: |
echo This job is independent from the other, it can run in parallel to either the build job or the test-job
run-generate:
needs: [linter, test]
uses: ./.github/workflows/generate.yaml
name: Generate
on:
workflow_call:
workflow_dispatch:
jobs:
generate:
runs-on: ubuntu-latest
name: Generate
steps:
- name: Generate
run: |
echo This job only runs if both the linter and the test jobs were successful.
echo This is set in the ci.yaml file calling this file
deploy:
runs-on: ubuntu-latest
name: Deploy
needs: [generate]
steps:
- name: Deploy
run: |
echo This job only runs if the Generate job was successful.
-
A job can declare in
needsone or more other jobs to be successful before running. -
In the CI.yaml file the
testjob depends on thebuildjob. Thelinterjob runs indpendently. -
The needed job must be in the same workflow (in the same YAML file) so the jobs in the generate.yaml file cannot depend the jobs in the
ci.yamlfile. -
However, One can make the
generate.yamlworkflow a reusable workflow by adding the triggerworkflow_calland then make the dependencies call it. -
In our case the
ci.yamlhas an extra job calledrun-generatethat depends on both thelinterand thetestjobs and runs the Generate workflow if both linter and test pass. -
Because the Generate workflow also has the
workflow_dispatchtrigger, one can run it from the GitHub UI in which case it will not “need” the CI job. Allowing this might or might not be a good idea.
Reuse a public GitHub Action workflow from another (public) repository.
name: Reusable
on:
workflow_call: # to make it reusable
#workflow_dispatch: # to allow manual triggering via the UI of Github
jobs:
generate:
runs-on: ubuntu-latest
name: Generate
steps:
- name: Generate
run: |
echo A reusable workflow
name: Reusing
on:
push:
#workflow_dispatch: # to allow manual triggering via the UI of Github
jobs:
regular:
runs-on: ubuntu-latest
name: Regular
steps:
- name: Regular
run: echo Just a regular job
run-reusable:
uses: ./.github/workflows/reusable.yaml
name: Reusing
on:
push:
#workflow_dispatch: # to allow manual triggering via the UI of Github
jobs:
#regular:
# runs-on: ubuntu-latest
# name: Regular
# steps:
# - name: Regular
# run: echo Just a regular job
run-reusable:
uses: szabgab/github-actions-reusable-workflow/.github/workflows/reusable.yaml@main
- Create Reusable Github Action workflow and use it in another workflow file
Bash
name: Shell
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v6
- name: Shell - View environment
run: |
uname -a
printenv | sort
Crystal
name: CI
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: '42 5 * * *'
# Created using https://crystal-lang.github.io/install-crystal/configurator.html
jobs:
test:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
- os: ubuntu-latest
crystal: 0.35.1
- os: ubuntu-latest
crystal: nightly
- os: macos-latest
- os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- name: Download source
uses: actions/checkout@v6
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
with:
crystal: ${{ matrix.crystal }}
- name: Install shards
run: shards update --ignore-crystal-version
- name: Run tests
run: crystal spec --order=random
- name: Check formatting
run: crystal tool format --check
if: matrix.crystal == null && matrix.os == 'ubuntu-latest'
Don’t run in forks
name: CI
# Don't run in forked repositories
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check if running in a fork
run: |
if [ "${{ github.repository_owner }}" != "Code-Maven" ]; then
echo "Running in a forked repository"
else
echo "Running in the original repository"
fi
# Convert to lower-case
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
if [ "$OWNER" != "code-maven" ]; then
echo "Running in a forked repository"
else
echo "Running in the original repository"
fi
- name: Run only in the "real" repository
if: ${{ github.repository_owner == 'Code-Maven' }}
run: |
echo "Running in the original repository"
- name: Run only in a forked repository
if: ${{ github.repository_owner != 'Code-Maven' }}
run: |
echo "Running in a forked repository owned by ${{ github.repository_owner }}"
deploy:
if: ${{ github.repository_owner == 'Code-Maven' }}
runs-on: ubuntu-latest
steps:
- name: Run only in the "real" repository
run: |
echo "Running in the original repository"
- Check the owner - don’t run in forks.
Cache restore and save
name: Save and Restore
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v3
- uses: actions/cache/restore@v3
id: restore
with:
path: data/
key: ${{ github.ref }}
#fail-on-cache-miss: False
- name: Pretend to add more data...
run: |
mkdir -p data
date >> data/time.txt
cat data/time.txt
- uses: actions/cache/save@v3
id: save
with:
path: data/
key: ${{ github.ref }}
- Cache restore and save (not doing what I wanted)
Run code if file changes
name: Shell
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v6
- name: Before cache
run: |
echo before
- name: Cache
id: cache-something
uses: actions/cache@v5
with:
path: already-built.txt
key: build-${{ hashFiles('requirements.txt', 'other.txt') }}
- name: Building database
run: ./build.sh
GitHub Actions with parameters
name: Shell
on:
workflow_dispatch:
inputs:
logLevel:
description: 'Log level'
required: true
default: 'warning'
type: choice
options:
- info
- warning
- debug
tags:
description: 'Test scenario tags'
required: false
type: boolean
environment:
description: 'Environment to run tests against'
type: string
required: true
parallel:
description: Number of processes in paralle
type: number
default: 1
required: true
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- name: Show parameters
run: |
echo Parameters
echo "Tags: ${{ inputs.tags }}"
echo "Environment: ${{ inputs.environment }}"
echo "logLevel: ${{ inputs.logLevel }}"
echo "parallel: ${{ inputs.parallel }}"
Incremental caching using S3 compatibale object storage of Linode
name: Save and Restore
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v6
- name: prepare
run: |
sudo apt-get update
sudo apt-get install -y s3cmd
echo "[default]" > s3cfg.ini
echo "host_base = us-southeast-1.linodeobjects.com" >> s3cfg.ini
echo "host_bucket = us-southeast-1.linodeobjects.com" >> s3cfg.ini
echo "access_key = ${{ secrets.ACCESS_KEY }}" >> s3cfg.ini
echo "secret_key = ${{ secrets.SECRET_KEY }}" >> s3cfg.ini
#- name: listing
# run: |
# echo restore
# s3cmd --config=s3cfg.ini ls s3://diggers/
# I had to put in an external script as GitHub runs the shell commands using the -e flag
# meaning if any of the commands fails then the whole step fails.
# The resote will fail the first time it run as there is still nothing to restore.
# So now we sweep in under the carpet.
# In a better solution we would look at the exit code and have a configuration option to
# disregard this failure.
- name: restore
run: ./restore.sh
- name: Pretend to add more data...
run: |
mkdir -p data
date >> data/time.txt
cat data/time.txt
- name: save
run: |
echo save
s3cmd --config=s3cfg.ini put data/time.txt s3://diggers/
Run only on the main branch and the pr/* branches
name: Run on main and and pr branches
# GitHub Workflow that will run on the `main` branch and on all the branches that are called `pr/*`
# This allows the developers to use any branch-name to either avoiding running the CI jobs (and incurring costs or hitting the parallel limitations)
# or to pick a branch name called `pr/SOMETHING` and making the CI job run.
# This might be interesting if you want to reduce the use of the CI in your oranization, but would like to make it easy
# for contributors to turn on GitHub Actions in their forks.
# So in-house developers would use any branchname except ones starting with `pr/` and contirbutors could use a branch name like `pr/SOMETHING`.
# The name `pr/` was picked arbitrarily. You could use any prefix there.
on:
push:
branches:
- main
- pr/*
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- name: Shell - View environment
run: |
echo "GITHUB REF: $GITHUB_REF"
echo "GITHUB_REF_NAME: $GITHUB_REF_NAME"
- Run only on the main branch and the pr/* branches to reduce CI runs in the organization, but allow CI runs for contributors in their forks
TODO: Experiment with GitHub Actions
-
Another repository we have not included yet.
-
And another one: repository
-
And a demo
name: Experiment
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v6
# with:
# fetch-depth: 21
#
# - name: Single command
# run: echo Hello World!
#
# - name: uname - pwd - ls
# run: |
# uname
# echo "-----"
# pwd
# echo "-----"
# ls -l
#
# - name: Show env
# run: |
# printenv | sort
#
# - name: Changed files
# run: |
# echo 2
# git diff --name-only HEAD~2
# echo 20
# git diff --name-only HEAD~20
# echo 21
# git diff --name-only HEAD~21
#
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v47
# NOTE: `since_last_remote_commit: true` is implied by default and falls back to the previous local commit.
- name: List all changed files
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
run: |
for file in ${ALL_CHANGED_FILES}; do
echo "$file was changed"
done
TODO Trigger on version tags
We can create a workflow that will only run when a tag was pushed out or when a tag staring with the letter v was pushed out.
on:
push:
tags:
- 'v*'
name: Git tags
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v6
- name: git log
run: |
git log
- name: show git tags
run: |
git tag
name: CI
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v6
- name: git log
run: |
git log
- name: show git tags
run: |
git tag
TODO Docker compose
name: CI
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '5 5 * * *'
jobs:
in_docker_compose:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Run docker-compose as a daemon
run: docker-compose up -d
- name: List docker containers
run: docker ps -a
- name: Ping the services to show network connectivity
run: |
docker exec github-actions-docker-compose_web_1 ping -c 1 mymongo
- name: Run Tests
run: |
docker exec github-actions-docker-compose_web_1 pytest -svv
- name: Stop the docker compose
run: docker-compose stop -t 0
Available GitHub actions
astral-sh/setup-uv
name: Setup uv
# https://github.com/astral-sh/setup-uv
on:
push:
pull_request:
workflow_dispatch:
jobs:
latest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: uv and python version
run: |
uv -V
python -V
try:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
uv-version:
- 0.8.10
- 0.7.3
python-version:
- 3.13
- 3.12
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: ${{matrix.uv-version}}
python-version: ${{matrix.python-version}}
enable-cache: false
- name: uv and python version
run: |
uv -V
python -V
GitHub Actions with service
Redis
name: CI
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: '42 5 * * *'
jobs:
test:
strategy:
fail-fast: false
matrix:
redis: ['6.0', '7.0', 'latest']
#redis: ['latest']
services:
redis:
image: redis:${{matrix.redis}}
runs-on: ubuntu-latest
container: ubuntu:22.10
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install curl and ping
run: |
apt-get update
apt-get install -y iputils-ping
apt-get install -y curl
apt-get install -y redis-tools
- name: ping redis
run: |
ping -c 4 redis
- name: Redis CLI
run: |
set -x
redis-cli -h redis get name
redis-cli -h redis set name "Foo Bar"
redis-cli -h redis get name
#- name: Redis with curl
# run: |
# set -x
# # curl http://redis:8001/
# # curl http://redis:8888/ping # Failed to connect to redis port 8888 - exit 7
# # curl -w '\n' http://redis:8888/ping
# # curl -X GET -H "accept: application/json" http://redis:9443/v1/bdbs -k -i # failed to connect - exit code 7
# # curl -X GET -H "accept: application/json" http://redis:6379/v1/bdbs -k -i # Empty reply from server - exit code 52
# # curl -X GET -H "accept: application/json" http://redis:6379/ping # Empty reply from server - exit code 52
# # curl -X GET -H "accept: application/json" http://redis:6379 # Empty reply from server - exit code 52
# # curl -X GET -H "accept: application/json" http://redis:6379/v1/get/name # Empty reply from server - exit code 52
Solr
name: CI
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: '42 5 * * *'
jobs:
test:
strategy:
fail-fast: false
matrix:
solr: ['latest']
services:
solr:
image: solr:${{matrix.solr}}
runs-on: ubuntu-latest
container: ubuntu:22.10
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install curl and ping
run: |
apt-get update
apt-get install -y curl
- name: Solr with curl
run: |
set -x
curl http://solr:8983/solr/
MySQL
name: CI
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '42 5 * * *'
jobs:
test:
strategy:
fail-fast: false
matrix:
mysql: ['latest']
services:
mysql:
env:
MYSQL_ROOT_PASSWORD: secret
image: mysql:${{matrix.mysql}}
runs-on: ubuntu-latest
container: ubuntu:25.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install curl and ping
run: |
apt-get update
apt-get install -y iputils-ping
apt-get install -y mysql-client
- name: ping mysql
run: |
ping -c 4 mysql
- name: MySQL CLI
run: |
set -x
echo "SELECT CURRENT_TIME" | mysql -h mysql --password=secret
echo "SELECT version()" | mysql -h mysql --password=secret
PostgreSQL
Sample GitHub Action workflow using a Postgres service so we can test our software that relies on a Postgres server.
name: CI
# Add some triggers
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '42 5 * * *'
jobs:
test:
strategy:
fail-fast: false
# List some of the tags from https://hub.docker.com/_/postgres
# You can be very specific or not so specific or just use the latest
matrix:
postgres:
- '16.13-trixie'
- '17'
- 'latest'
# The `services` will launch a separate docker container using the given image
# That we take from the matrix
services:
# We can name our service anything we want. In this case we called it `mypg`.
# We'll use the same name later.
mypg:
image: postgres:${{matrix.postgres}}
env:
# We defined some environment variables needed by the the server.
# They are also used in the client part of this job later in this file.
POSTGRES_USER: myusername
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
# Some magic options needed for the Postgres in the Docker container
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
# This is where we define where and how our code runs
runs-on: ubuntu-latest
# It is easier if we also use a Docker container
container: ubuntu:25.10
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install curl and ping
# Install some tools to demonstrate connectivity to the database
run: |
apt update
apt install -y iputils-ping
apt install -y postgresql-client
- name: ping postgres
# The hostname of the postgres server is the name we gave to the service: "mypg"
run: |
ping -c 4 mypg
- name: PostgreSQL CLI psql
# We set the same "secret" as we set in the service.
env:
PGPASSWORD: secret
# use the username and database name we set in the service above.
# A few sample SQL statements to show we really have access to Postgres
run: |
set -x # show the commands
echo "SELECT CURRENT_TIME" | psql -h mypg -U myusername mydb
echo "SELECT version()" | psql -h mypg -U myusername mydb
echo "CREATE TABLE counter (name text, cnt int)" | psql -h mypg -U myusername mydb
echo "INSERT INTO counter (name, cnt) VALUES ('foo', 1)" | psql -h mypg -U myusername mydb
echo "INSERT INTO counter (name, cnt) VALUES ('bar', 42)" | psql -h mypg -U myusername mydb
echo "SELECT * FROM counter" | psql -h mypg -U myusername mydb
Reusabel GitHub Actions
Dependabot
Configure GitHub to check if any of the dependencies of your project can be or should be updated.
Dependabot for Python and GitHub Actions
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
groups:
python:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
groups:
github-actions:
patterns:
- "*"
GitHub Actions for Perl
Each Perl project uploaded to CPAN has a configuration file that deal with packaging and installations. There are several possibilities and base on the file we’ll need a different workflow.
- If the repository has a file called
dist.iniin the root then the project uses Dist::Zilla. We have two examples for this case. - If the repository has a file called
Build.PLin the root then the project uses Module::Build. We have an example for this case. - If the repository has a file called
Makefile.PLin the root then the project either uses ExtUtils::MakeMaker or Module::Install. We have two examples for this case.
If the project has both dist.ini and Makefile.PL then most likely the latter was generated and as a generated file it should not be in git. Some authors add it to make it easier for contributors to run the tests without installing the whole Dist::Zilla toolchain. For the CI I’d recommend using one of the Dist::Zilla workflows.
The same applies if the project has both Build.PL and Makefile.PL.
If none of the above situation applies then talk to me. I’ll try to figure it out and help.
Native or Docker?
The Docker image contains a lot of 3rd party libraries and so using a workflow with the Docker image will probably make the CI much faster. However that is Linux only.
If you’d like to ensure that your code runs on Windows and macOS then you need the native workflow.
You can setup both and combine them.
I think the ideal would be to
- Run the test in Docker in one or more versions of perl.
- Create a distribution using one of the Perl versions.
- Upload it as an artifact.
- Run another job on the native OSes using the zip file created earlier.
I already setup such a workflow for Test::Class. I’ll add it here as an example.
Goals
- Setup CI (Continuous Integration) for CPAN modules.
- Setup CI for non-CPAN Perl code.
- Collect data and generate web site using GitHub Pages.
CPAN Testers
CPAN Testers is an excellent service offered by volunteers of the Perl community. The volunteers have bot that monitor the upload queue of CPAN, run the tests of the recently uploaded modules and report the results to a central database.
You can see the results on the on their web site and also when you visit the page of a module on MetaCPAN you will see the stats of the “Testers” in the side-bar. e.g. DBI
So why should we also set up GitHub Actions?
A few reasons:
- You get faster feedback. You get feedback already during development, not only after uploading a package to CPAN.
- Tests can run on pull-requests as well giving fast feedback to contributors.
- You can make customization and special setups (e.g. setup a Postgres database to use during the testing)
- You can use secrets to connect to external services (e.g. APIs).
Perl with Makefile.PL run native
name: CI
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '42 5 * * 0'
jobs:
perl-job:
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- macos-latest
- windows-latest
perl:
- '5.30'
- '5.42'
runs-on: ${{matrix.runner}}
name: OS ${{matrix.runner}} Perl ${{matrix.perl}}
steps:
- uses: actions/checkout@v6
- name: Set up perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: ${{ matrix.perl }}
distribution: ${{ ( startsWith( matrix.runner, 'windows-' ) && 'strawberry' ) || 'default' }}
- name: Install dependencies
run: |
cpanm --notest Module::Install --version
cpanm --installdeps --notest --version .
- name: Run Tests on Linux and macOS
if: ${{ matrix.runner != 'windows-latest' }}
run: |
perl Makefile.PL
make
make test
- name: Run Tests on Windows
if: ${{ matrix.runner == 'windows-latest' }}
run: |
perl Makefile.PL
gmake
gmake test
Perl with Makefile.PL using the perl-tester Docker image
name: CI Makefile
on:
push:
pull_request:
workflow_dispatch:
#schedule:
# - cron: '42 5 * * 0'
jobs:
perl-job:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
perl-version:
# - '5.8'
# - '5.30'
- '5.42'
# - 'latest'
container:
image: perldocker/perl-tester:${{ matrix.perl-version }} # https://hub.docker.com/r/perldocker/perl-tester
name: Perl ${{ matrix.perl-version }}
steps:
- uses: actions/checkout@v6
- name: Regular tests
run: |
cpanm --installdeps --notest .
perl Makefile.PL
make
make test
- name: Prepare for release tests
run: |
cpanm --installdep .
cpanm --notest Test::CheckManifest Test::Pod::Coverage Pod::Coverage Test::Pod
- name: Release tests
env:
RELEASE_TESTING: 1
run: |
perl Makefile.PL
make
make test
The perldocker/perl-tester image and its source on GitHu
Perl with Build.PL
name: CI Build
on:
push:
pull_request:
workflow_dispatch:
#schedule:
# - cron: '42 5 * * 0'
jobs:
perl-job:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
perl-version:
# - '5.8'
# - '5.30'
- '5.42'
# - 'latest'
container:
image: perldocker/perl-tester:${{ matrix.perl-version }} # https://hub.docker.com/r/perldocker/perl-tester
name: Perl ${{ matrix.perl-version }}
steps:
- uses: actions/checkout@v6
- name: Regular tests
run: |
perl Build.PL
perl Build test
Perl with Dist::Zilla Native
name: CI on Native
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '42 5 * * *'
jobs:
test:
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- macos-latest
- windows-latest
perl:
# We need quotes or the 5.30 will be considered 5.3
- '5.30'
- '5.42'
#exclude:
# - runner: windows-latest
# perl: '5.36'
runs-on: ${{matrix.runner}}
name: OS ${{matrix.runner}} Perl ${{matrix.perl}}
steps:
- uses: actions/checkout@v6
- name: Set up perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: ${{ matrix.perl }}
distribution: ${{ ( startsWith( matrix.runner, 'windows-' ) && 'strawberry' ) || 'default' }}
- name: Show Perl Version
run: |
perl -v
- name: Install Dist::Zilla
run: |
cpanm -v
cpanm --notest --verbose Dist::Zilla
- name: Install Modules
run: |
dzil authordeps --missing | cpanm --notest --verbose
dzil listdeps --develop --missing | cpanm --notest --verbose
dzil listdeps --author --missing | cpanm --notest --verbose
- name: Run tests
env:
AUTHOR_TESTING: 1
RELEASE_TESTING: 1
run: |
dzil test --author --release
Perl with Dist::Zilla in Docker
name: CI in Docker
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '42 5 * * *'
jobs:
test:
strategy:
fail-fast: false
matrix:
perl-version:
- '5.30'
- '5.42'
runs-on: ubuntu-latest
name: OS Perl ${{matrix.perl-version}}
container:
image: perldocker/perl-tester:${{ matrix.perl-version }}
# https://hub.docker.com/r/perldocker/perl-tester
steps:
- uses: actions/checkout@v6
- name: Show Perl Version
run: |
perl -v
- name: Install Dist::Zilla
run: |
cpanm -v
cpanm --notest --verbose Dist::Zilla
- name: Install Modules
run: |
dzil authordeps --missing | cpanm --notest --verbose
dzil listdeps --develop --missing | cpanm --notest --verbose
dzil listdeps --author --missing | cpanm --notest --verbose
- name: Run tests
env:
AUTHOR_TESTING: 1
RELEASE_TESTING: 1
run: |
dzil test --author --release
Perl with Test coverage report
name: CI
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '42 5 * * *'
jobs:
tests:
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
#- macos-latest
#- windows-latest
perl:
- '5.30'
- '5.42'
#exclude:
# - runner: windows-latest
# perl: '5.36'
runs-on: ${{matrix.runner}}
name: OS ${{matrix.runner}} Perl ${{matrix.perl}}
steps:
- uses: actions/checkout@v6
- name: Set up perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: ${{ matrix.perl }}
distribution: ${{ ( matrix.runner == 'windows-latest' && 'strawberry' ) || 'default' }}
- name: Show Perl Version
run: |
perl -v
- name: Install Modules
run: |
cpanm -v
cpanm --installdeps --with-develop --notest .
# --with-configure
# --with-recommends, --with-suggests
- name: Run tests
env:
AUTHOR_TESTING: 1
RELEASE_TESTING: 1
run: |
perl Makefile.PL
make
make test
coverage:
runs-on: ubuntu-latest
needs: tests
name: Test Coverage
container:
image: perldocker/perl-tester:5.42
steps:
- uses: actions/checkout@v6
- name: Generate test coverage
env:
AUTHOR_TESTING: 1
RELEASE_TESTING: 1
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
perl Makefile.PL
make
# cpanm --notest Devel::Cover
# cpanm --notest Devel::Cover::Report::Coveralls
cover -v
perl -MDevel::Cover::Report::Coveralls -E 'say $Devel::Cover::Report::Coveralls::VERSION'
cover -test -report coveralls
Examples - Perl
- docker-perl-tester - build a Docker image and upload to Docker Hub.
- Test2-Harness
- Perl Power Tools
Perl Tester Docker Image
CI Perl Tester Helpers
Set of scripts (available via action syntax as well), all of them included in perltester dockers
GitHub Action to setup perl environment in the marketplace
Perl and OS matrix
name: OS and Perl Matrix
on:
push:
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- macos-latest
- windows-latest
perl:
- '5.32'
- '5.30'
runs-on: ${{matrix.runner}}
name: OS ${{matrix.runner}} Perl ${{matrix.perl}}
steps:
- uses: actions/checkout@v6
- name: Set up perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: ${{ matrix.perl }}
#distribution: strawberry
# - name: Set up perl on Windows
# if: ${{ startsWith( matrix.runner, 'windows-' ) }}
# uses: shogo82148/actions-setup-perl@v1
# with:
# perl-version: ${{ matrix.perl }}
# distribution: ${{ ( startsWith( matrix.runner, 'windows-' ) && 'strawberry' ) || 'default' }}
- name: Show Perl Version
run: |
perl -v
- name: View environment
run: |
uname -a
printenv | sort
perl -v
#- name: Install cpanm
# if: ${{ matrix.runner != "windows-latest" }}
# run: |
# curl -L https://cpanmin.us | perl - App::cpanminus
- name: Install module
run: |
cpanm --verbose Module::Runtime
# - name: Regular Tests
# run: |
# perl Makefile.PL
# make
# make test
The Perl Planetarium
About Github Action for Perl
- Presentation of Olaf Alders
- Slides of Olaf Alders
GitHub Actions for Python
Python
- A demo to show a simple task before we start learning about the YAML files from scratch
name: Python
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v2
- name: Install dependencies
run: pip install -r requirements.txt
- name: Check Python version
run: python -V
- name: Test with pytest
run: pytest
pytest
def test_demo():
assert True
Examples - Python
Python with Matrix
name: CI
on:
push:
pull_request:
workflow_dispatch:
# schedule:
# - cron: '42 5 * * *'
jobs:
test:
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11"]
runs-on: ${{matrix.runner}}
name: OS ${{matrix.runner}} Python ${{matrix.python-version}}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest
# - name: Install project
# run: |
# pip install -e .
- name: Check Python version
run: python -V
- name: Test with pytest
run: pytest -svv
GitHub Actions for Rust
name: Default on Ubuntu
on:
- pull_request
- push
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: rustup --version
- run: rustc -vV
- run: cargo clippy -- --deny clippy::pedantic
- run: cargo fmt --all -- --check
- run: cargo test
name: Default on Windows
on:
- pull_request
- push
jobs:
main:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- run: rustup --version
- run: rustc -vV
- run: cargo clippy -- --deny clippy::pedantic
- run: cargo fmt --all -- --check
- run: cargo test
name: Default on macOS
on:
- pull_request
- push
jobs:
main:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- run: rustup --version
- run: rustc -vV
- run: cargo clippy -- --deny clippy::pedantic
- run: cargo fmt --all -- --check
- run: cargo test
name: Matrix
on:
- pull_request
- push
jobs:
main:
strategy:
matrix:
rust:
- stable
- beta
- nightly
- 1.78
- 1.88
name: ${{matrix.rust}}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{matrix.rust}}
components: rustfmt, clippy
- run: rustup --version
- run: rustc -vV
- run: cargo clippy -- --deny clippy::pedantic
- run: cargo fmt --all -- --check
- run: cargo test
# - run: cargo install cargo-tarpaulin && cargo tarpaulin --out Xml
# - uses: codecov/codecov-action@v1
Rust with test coverage
name: Default on Ubuntu
on:
- pull_request
- push
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: rustup --version
- run: rustc -vV
- run: cargo clippy -- --deny clippy::pedantic
- run: cargo fmt --all -- --check
- run: cargo test
name: Matrix
on:
- pull_request
- push
jobs:
main:
strategy:
matrix:
rust:
- stable
# - beta
# - nightly
# - 1.78
# - 1.88
name: ${{matrix.rust}}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{matrix.rust}}
components: rustfmt, clippy
- run: rustup --version
- run: rustc -vV
- run: cargo clippy -- --deny clippy::pedantic
- run: cargo fmt --all -- --check
- run: cargo test
- run: cargo install cargo-tarpaulin
- run: cargo tarpaulin --out Xml
- uses: codecov/codecov-action@v5
GitHub Actions for NodeJS
NodeJS and OS matrix
name: CI
on: push
jobs:
test:
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, macos-latest, windows-latest]
nodejs: [ 14.15, 12 ]
runs-on: ${{matrix.runner}}
name: OS ${{matrix.runner}} NodeJS ${{matrix.nodejs}}
steps:
- uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.nodejs }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.nodejs }}
- name: Show NodeJS Version
run: |
node -v
Coveralls
About Coveralls
GitHub Actions Case studies
-
Perl Test::Class - create release on one platform and then test it on many source
-
Perl DBD::Pg the PostgreSQL database driver - with PostgreSQL service source
-
Perl Planetarium collect RSS Feeds and build a web site.
-
PyDigger - collect and store
TODO Collect GitHub Actions
The collect GitHub Actions project is just a skeleton to, well collect and analyze GitHub action configuration files.
Previous Sessions
2020.10.29
Blank
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the main branch
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# Runs a single command using the runners shell
- name: Run a one-line script
run: echo Hello, world!
# Runs a set of commands using the runners shell
- name: Run a multi-line script
run: |
echo Add other actions to build,
echo test, and deploy your project.
Environment variables
name: Matrix environment variables
on: push
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
fruit:
- Apple
- Banana
meal:
- Breakfast
- Lunch
steps:
- name: Single step
env:
DEMO_FRUIT: ${{ matrix.fruit }}
DEMO_MEAL: ${{ matrix.meal }}
run: |
echo $DEMO_FRUIT for $DEMO_MEAL
Linux
name: Minimal Ubuntu
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Single step
run: echo Hello World
- name: Look around
run: |
uname -a
pwd # /home/runner/work/REPO/REPO
whoami # runner
uptime
which perl
which python
which python3
which ruby
which node
which java
perl -v
python -V
python3 -V
ruby -v
node -v
javac -version
java -version
macOS
name: Minimal MacOS
on: push
jobs:
build:
runs-on: macos-latest
steps:
- name: Single step
run: echo Hello World
- name: Look around
run: |
uname -a
pwd
whoami
uptime
which perl
which python
which python3
which ruby
which node
which java
perl -v
python -V
python3 -V
ruby -v
node -v
javac -version
java -version
Windows
name: Minimal Windows
on: push
jobs:
build:
runs-on: windows-latest
steps:
- name: Single step
run: echo Hello World
- name: Look around
run: |
uname -a
pwd
whoami
uptime
which perl
which python
#which python3
which ruby
which node
which java
perl -v
python -V # 3.7.9
#python3 -V
ruby -v
node -v
javac -version
java -version
2020.12.24
name: CI
on:
push:
branches: '*'
pull_request:
branches: '*'
workflow_dispatch:
# schedule:
# - cron: '42 5 * * *'
jobs:
build:
environment: DEPLOYMENT
runs-on: ubuntu-latest
name: Build on Linux
steps:
- uses: actions/checkout@v2
- name: Print
run: |
hostname
printenv | sort
- name: Deploy webhook
#env:
# DEPLOY_SECRET: ${{ secrets.DEPLOY_SECRET }}
run: |
#echo $DEPLOY_SECRET
echo ${{secrets.DEPLOY_SECRET}}
curl --silent --data "secret=${{secrets.DEPLOY_SECRET}}" --data "GITHUB_REPOSITORY=$GITHUB_REPOSITORY" --data "GITHUB_SHA=$GITHUB_SHA" http://104.236.40.108/action
- name: Deploy SSH
env:
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
KNOWN_HOSTS: ${{ secrets.KNOWN_HOSTS }}
run: |
mkdir ~/.ssh/
echo "$PRIVATE_KEY" > ~/.ssh/id_rsa
echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/id_rsa
ls -l ~/.ssh/
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@104.236.40.108 hostname
2026.03.08
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v6
# Runs a single command using the runners shell
- name: Run a one-line script
run: echo Hello, world!
# Runs a set of commands using the runners shell
- name: Run a multi-line script
run: |
echo Add other actions to build,
echo test, and deploy your project.
echo another line