GitHub Actions Tips and Tricks

This is a companion post to a talk I recently gave. You'll find a recap of the content, plus code examples, helpful links, and tips the slides couldn't fit in the slides. Before you go further, I'd love to connect on social media. You

This is a companion post to a talk I recently gave. You’ll find a recap of the content, plus code examples, helpful links, and tips the slides couldn’t fit in the slides.

Before you go further, I’d love to connect on social media. You can find me @mackenly on X, @mackenly on GitHub, and @mackenly on LinkedIn. You can also find me here on my blog, crtv.dev, or on my personal site mackenly.com. Look forward to connecting!

The Problem

Every good automation starts with a manual process. My problem was that every time I wanted a brand icon on my Stream Deck I had to:

  1. Go to simpleicons.org and search for the icon
  2. Download the SVG to my computer and convert to a PNG
  3. Manually import it into the Stream Deck application
  4. Repeat for every. single. icon.

As a developer, I knew there had to be a better way.

Stream Desk Marketing Photo

The Solution

The result was Simple Icons Streamdeck, an icon pack that imports directly into Stream Deck’s software so you can browse, search, and apply icons without ever leaving the app. No downloading, no manual importing.

Here’s a rough timeline:

  • 2022 — Built the initial script in an afternoon
  • 2022 — Added to the Elgato Marketplace
  • 2022 — Automation took over maintenance entirely
  • 2026 — 60,000+ users, 1 maintainer, ~0 hours/week spent on upkeep

Notice how there’s nothing in between when I built in and now…

The lesson: solve your own problem first, then automate the maintenance so you can walk away.

The Maintenance Trap

The Project Killer ( duh, duh, duuuuh)

When a side project gains users, it gains weight. Things that were fine with one person (me) wouldn’t work if I wanted to share this project publically:

  • Upstream changes: Simple Icons releases updates regularly: new icons, removed icons, renamed slugs. Without automation, every release means a manual review session.
  • Compliance requirements: If Simple Icons removes an icon for copyright/trademark reasons, I need to ship that change fast. Manual processes are too slow.
  • Time cost: This project earns no revenue. My maintenance budget is zero hours per week. Automation isn’t optional; it’s the only viable model.
  • Growing expectations: 60K users means more issues, more bug reports, more pressure. Automation creates a buffer between user volume and maintainer bandwidth.

The answer to all of these? GitHub Actions.

How GitHub Actions Work

A workflow is just a YAML file in .github/workflows/. At a high level, every workflow follows the same four-part structure:

flowchart LR T([Trigger]) --> W[Workflow] W --> J1[Job A] W --> J2[Job B] J1 --> S1[Step 1] J1 --> S2[Step 2] J2 --> S3[Step 1] J2 --> S4[Step 2]
Trigger → Workflow → Job → Steps
plaintext
Concept What it is
Trigger What starts the workflow: push, pull_request, schedule, workflow_dispatch, webhook
Workflow The YAML file itself, living in .github/workflows/
Job Runs on a virtual machine (runner). A workflow can have multiple jobs.
Step An individual shell command or reusable action inside a job

Minimal example:

on: [push]

jobs:
  hello:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Hello from GitHub Actions"
yaml

That’s it. From here, you can build anything.

GitHub Actions documentation, Workflow syntax reference

Practical Example: Automated Testing

Run your test suite on every push and pull request, across multiple Node.js versions using a matrix strategy:

# .github/workflows/ci.yml
on: [push, pull_request]

jobs:
  test:
    name: Test (Node ${{ matrix.node }})
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: ['20', '22', '24']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run build
yaml

No duplicating code for every mutation or trying to run locally.

Use npm ci instead of npm install in CI. It’s faster, stricter, and uses the lockfile exactly — no surprises.

actions/setup-node, Matrix strategies

Practical Example: Automated Releases

Push a version tag, go to sleep. Wake up to a fully published release with changelogs, packaged assets, and zero manual steps.

# .github/workflows/release.yml
on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Build
        run: npm ci && npm run build

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: dist/**
yaml

The flow:

  1. Tag push — git tag v1.2.3 && git push --tags
  2. Build — Compile, bundle, and package
  3. Changelog — Auto-generated from commit history
  4. Publish — GitHub release created with assets attached

softprops/action-gh-release, Automatically generated release notes

Practical Example: Scheduled Automation

Use the schedule trigger with a cron expression to run workflows on a timer. This is the backbone of my zero-maintenance setup.

on:
  schedule:
    - cron: '0 6 * * *'    # Every day at 6 AM UTC
yaml

Cron cheat sheet: minute hour day-of-month month day-of-week. Use crontab.guru to build and verify expressions.

What you can schedule:

Use case What it does
Dependency sync Check upstream repos for changes, auto-create PRs with updates
Security scans Run vulnerability checks nightly against your dependencies
Health checks Ping endpoints, verify downloads work, alert on failures
Data freshness Pull latest data from APIs and commit updates automatically

Example: nightly dependency update check

on:
  schedule:
    - cron: '0 2 * * 1'    # Every Monday at 2 AM UTC

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check for upstream changes
        run: ./scripts/check-upstream.sh
      - name: Create PR if changes found
        uses: peter-evans/create-pull-request@v6
        with:
          commit-message: 'chore: sync upstream changes'
          title: 'Automated upstream sync'
yaml

Scheduled workflow triggers, peter-evans/create-pull-request

Practical Example: Issue & PR Automation

GitHub Actions can also automate your repo.

Auto-Labeler

Automatically label PRs based on which files changed. Frontend changes? Slap a frontend label on it. No human needed.

# .github/workflows/label.yml
on: [pull_request]

jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/labeler@v5
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
yaml

Pair it with a .github/labeler.yml config file that maps file patterns to labels.

actions/labeler

Stale Bot

Close issues that have been inactive for 30 days with a friendly reminder. There are pros and cons to this, so use with caution!

# .github/workflows/stale.yml
on:
  schedule:
    - cron: '0 0 * * *'

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/stale@v9
        with:
          stale-issue-message: 'This issue has been inactive for 30 days. It will be closed in 7 days unless there is new activity.'
          days-before-stale: 30
          days-before-close: 7
yaml

actions/stale

Auto-Merge Dependabot PRs

Automatically approve and merge dependency updates that pass CI. No more reviewing 40 Dependabot PRs manually.

# .github/workflows/auto-merge.yml
on: pull_request

jobs:
  auto-merge:
    runs-on: ubuntu-latest
    if: github.actor == 'dependabot[bot]'
    steps:
      - name: Auto-merge patch and minor updates
        run: gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
yaml

Safety note: Only auto-merge if you have a solid test suite. The CI check is your safety net here.

Automating Dependabot

Beyond the Basics

Once you’re comfortable with the fundamentals, these patterns unlock a lot:

Self-Updating Profile README

Use a scheduled action to pull live stats. Bring in download counts, GitHub stars, or the latest release, then commit an updated README.md daily. Your project page is always current without you touching it.

- name: Update stats
  run: python scripts/update-readme.py
- name: Commit changes
  run: |
    git config --global user.name 'github-actions[bot]'
    git config --global user.email 'github-actions[bot]@users.noreply.github.com'
    git add README.md
    git diff --staged --quiet || git commit -m 'chore: update stats'
    git push
yaml

AI-Powered Code Review

Trigger an LLM-based reviewer on every PR to catch style issues, suggest improvements, and summarize changes for human reviewers. There are a lot of SaaS products that do this for you, and most of the AI providers have some form of this.

Metrics Dashboard Generator

Collect performance benchmarks, test coverage, and bundle sizes on each push. Commit the data and generate a static site with trend charts automatically using tools like github-action-benchmark.

Cross-Repo Orchestration

When your library publishes a new version, automatically open PRs in all downstream repos that depend on it. Monorepo benefits without the monorepo complexity.

- name: Trigger downstream updates
  uses: peter-evans/repository-dispatch@v3
  with:
    token: ${{ secrets.PAT }}
    repository: your-org/downstream-repo
    event-type: library-updated
    client-payload: '{"version": "${{ github.ref_name }}"}'
yaml

repository-dispatch event

Tips & Tricks

Workflow Design

Start simple, iterate. Begin with a single workflow file. Add complexity only when you actually need it. A working simple pipeline beats a broken complex one every time.

Use the Actions Marketplace. Don’t reinvent the wheel. Thousands of pre-built actions exist for common tasks. Always pin to a specific version (e.g., @v4) for stability — never use @main or @latest in production.

GitHub Actions Marketplace

Cache aggressively. actions/cache can shave minutes off every run. Cache node_modules, pip packages, build artifacts, or anything that doesn’t change between runs.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: ${{ runner.os }}-node-
yaml

actions/cache documentation

Use environment secrets. Never hardcode tokens or credentials. GitHub’s encrypted secrets are free and easy to use. For cloud providers (AWS, GCP, Azure), use OIDC-based authentication to avoid long-lived credentials entirely.

GitHub encrypted secrets, OIDC with cloud providers

Debugging & Billing

Use act for local testing. The act tool runs GitHub Actions locally in Docker. Debug without pushing 50 commits just to test your workflow logic.

# Install act (macOS)
brew install act

# Run a specific workflow
act push -W .github/workflows/ci.yml
bash

nektos/act

Set timeout-minutes. The default job timeout is 6 hours. A stuck job will happily burn through your free minutes. I once spent thousands of minutes because an action that ran on every commit got hung up. Set realistic timeouts:

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15
yaml

Setting timeout-minutes

Use matrix strategies wisely. Test across OS and version combinations with matrix builds, but add fail-fast: true so one failure stops the whole matrix instead of burning compute on doomed jobs.

strategy:
  fail-fast: true
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    node: ['20', '22']
yaml

Matrix strategies

Extract reusable workflows. If you repeat the same job pattern across multiple repos or workflows, pull it into a reusable workflow file and call it with workflow_call. DRY principles apply to YAML too.

# Calling a reusable workflow
jobs:
  test:
    uses: your-org/.github/.github/workflows/test.yml@main
    with:
      node-version: '22'
yaml

Reusable workflows

Key Takeaways

  1. Solve your own problems first. The best side projects come from personal frustration. Build what you actually need.
  2. Automate the boring stuff. If you do it more than twice, write a workflow. Future-you will be grateful.
  3. Sustainable maintenance is essential. You are your most important user. If maintaining it burns you out, the project dies.
  4. GitHub Actions are cheap, easy, and powerful. They can run anything you can code: CI, releases, scheduled tasks, and more.
  5. Start simple, ship fast, iterate. A working system beats a perfect system. Add complexity only when you need it.

Official Docs

Essential Actions

Tools

The Project

#devops