Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  • python-with-test

  • python-without-test

  • Fork the repository so you have your own copy

  • Clone the forked repo

  • Enable Travis, Appveyor

  • Add tests

  • Add badges to the README.md of 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/workflows folder.

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 .yaml or .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 Actions tab 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...

repository


  • 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

repository

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

repository

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

repository

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

repository

Name of a workflow

  • The name is just some free text to help identify the workflow.
name: Free Text defaults to the filename

Triggering jobs

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_dispatch to 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:
   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


repository

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

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"

repostory

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"

repository

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

  • matrix

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

repository

  • 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

repository

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

repository

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.

  • lock-threads

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

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

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.

  • repository

  • A job can declare in needs one or more other jobs to be successful before running.

  • In the CI.yaml file the test job depends on the build job. The linter job 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.yaml file.

  • However, One can make the generate.yaml workflow a reusable workflow by adding the trigger workflow_call and then make the dependencies call it.

  • In our case the ci.yaml has an extra job called run-generate that depends on both the linter and the test jobs and runs the Generate workflow if both linter and test pass.

  • Because the Generate workflow also has the workflow_dispatch trigger, 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

repository

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

repository

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'

repository

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"

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 }}

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"

TODO: Experiment with GitHub Actions

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

repository

TODO Docker compose

repository

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

source of astral-sh/setup-uv

repository

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.ini in the root then the project uses Dist::Zilla. We have two examples for this case.
  • If the repository has a file called Build.PL in the root then the project uses Module::Build. We have an example for this case.
  • If the repository has a file called Makefile.PL in 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

  1. Run the test in Docker in one or more versions of perl.
  2. Create a distribution using one of the Perl versions.
  3. Upload it as an artifact.
  4. 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

repository

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

repository

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

repository

Examples - Perl

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

repository

The Perl Planetarium

About Github Action for Perl

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

repository

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

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

repository

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

repository

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

repository