Minimalist GitHub Actions: Your workflows should do less

Malcolm Matalka avatar

Malcolm Matalka

Minimalist GitHub Actions: Your workflows should do less blog post

I’ve seen a lot of GitHub Actions workflow files, and they always start off with the best of intentions. 10 lines of lean, well-structured, and intentional workflow. Fast forward two years later, and now it’s 200 lines of nested conditions, environment variables, and custom actions that nobody wants to touch. All of this creates a lot of technical debt and toil.

IMO, there’s a much better way to structure your workflows to avoid this rat’s nest: move your workflow logic into external scripts and keep your GitHub Actions workflows minimal.

By migrating your bloated workflows into clean, portable automation that your team can actually understand and safely modify, you not only simplify your workflows but also prevent vendor lock-in. Additionally, you can test all of your CI/CD processes in your local environment.

The Problem with Complex Workflows

Chaotic GitHub Actions workflows create many challenges and headaches for engineering teams. When workflows contain intricate logic directly in YAML, they become extremely difficult to understand and dangerous to change. YAML lacks proper tooling and is prone to error.

Let’s take this complex workflow file as an example:

name: Complex Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Complex Conditional
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "::set-output name=env::prod"
elif [[ "${{ github.ref }}" =~ ^refs/heads/release/ ]]; then
echo "::set-output name=env::staging"
else
echo "::set-output name=env::dev"
fi
- name: Deploy
uses: custom/deploy-action@v1
with:
environment: ${{ steps.complex_conditional.outputs.env }}
config: ${{ toJSON(fromJSON(steps.complex_conditional.outputs.config)) }}

This workflow embeds environment detection logic directly in YAML. Please don’t do this. It’s difficult to test and debug, and when logic errors occur, the nested structure of YAML makes it hard to pinpoint the exact failure point.

Baking all of your logic into your workflow file also creates vendor lock-in. All of this logic is now tied to GitHub Actions, making it much harder to migrate to alternative CI/CD platforms in the future. It also complicates maintaining consistency across different deployment environments.

Building Minimal Workflows

The core principle of minimal workflows is simple: delegate all of your complex business logic to external scripts inside your repository. Keep workflow files focused on orchestration, creating a clear separation between CI/CD coordination and business logic.

Here’s an example of a shell script that handles common CI/CD tasks:

#!/bin/bash
set -euo pipefail
# Function to run tests
run_tests() {
echo "Running tests..."
npm test
}
# Function to build application
build_app() {
echo "Building application..."
npm run build
}
# Main execution
main() {
run_tests
build_app
}
main "$@"

This script has proper error handling, clear function organization, and modular design. These attributes make it reusable across different CI/CD platforms and easier to maintain.

Our workflow file then becomes a simple orchestrator:

name: Not a Crazy CI Pipeline
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Run CI pipeline
run: ./scripts/ci.sh

Benefits of Minimal Workflows

No Vendor Lock-In

By approaching workflows this way, you’re creating true portability and flexibility for your CI/CD pipeline. A Python or shell script that manages deployments can run consistently in GitHub Actions, CircleCI, Jenkins, Drone, etc. This gives teams the option to easily switch between CI/CD providers without large rewrites.

Local Testing and Debugging

Scripts that live inside your repository can be run locally on your workstation. This is extremely useful for testing automation locally instead of pushing changes to trigger GitHub Actions. When problems pop up, developers can easily run and debug scripts on their machines rather than attempting to replicate complex workflow environments.

Workflows Your Team Actually Understands

Minimal workflows make it significantly easier to understand how pipelines function. Instead of requiring developers to learn GitHub Actions workflow syntax, they can work with familiar programming languages in external scripts that live in your repository. A new developer can understand a Python or Bash deployment script much more easily than wading through complex YAML with actions, conditional logic, etc.

Creating Your First Minimal Workflow

Start by pulling out all of the inline commands in your workflow files and moving them into separate scripts. Remember that your GitHub Actions workflows should orchestrate rather than implement. Keep it simple, stupid.

By embracing minimal workflows, you create CI/CD processes that stand the test of time and team growth. Your automation becomes more maintainable, portable, and understandable, all while avoiding the pitfalls of complex YAML configurations. Also, people end up liking you more.

GitOps-First Infrastructure as Code

Ready to get started?

Build, manage, and deploy infrastructure with GitHub pull requests.