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:
- Go to simpleicons.org and search for the icon
- Download the SVG to my computer and convert to a PNG
- Manually import it into the Stream Deck application
- 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:
Trigger → Workflow → Job → Steps| 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"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 buildNo duplicating code for every mutation or trying to run locally.
Use
npm ciinstead ofnpm installin 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/**The flow:
- Tag push —
git tag v1.2.3 && git push --tags - Build — Compile, bundle, and package
- Changelog — Auto-generated from commit history
- 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 UTCCron 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'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 }}Pair it with a .github/labeler.yml config file that maps file patterns to labels.
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: 7Auto-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 }}Safety note: Only auto-merge if you have a solid test suite. The CI check is your safety net here.
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 pushAI-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 }}"}'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.
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-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.ymlSet 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: 15Use 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']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'Key Takeaways
- Solve your own problems first. The best side projects come from personal frustration. Build what you actually need.
- Automate the boring stuff. If you do it more than twice, write a workflow. Future-you will be grateful.
- Sustainable maintenance is essential. You are your most important user. If maintaining it burns you out, the project dies.
- GitHub Actions are cheap, easy, and powerful. They can run anything you can code: CI, releases, scheduled tasks, and more.
- Start simple, ship fast, iterate. A working system beats a perfect system. Add complexity only when you need it.
Helpful Links
Official Docs
- GitHub Actions documentation
- Workflow syntax reference
- Events that trigger workflows
- GitHub Actions Marketplace
- Billing for GitHub Actions
Essential Actions
- actions/checkout Check out your repo
- actions/setup-node Set up Node.js
- actions/cache Cache dependencies
- actions/labeler Auto-label PRs
- actions/stale Close stale issues
- softprops/action-gh-release Create releases
- peter-evans/create-pull-request Open PRs programmatically
Tools
- act Run Actions locally
- crontab.guru Build cron expressions
- Simple Icons The upstream project this whole story is about
The Project
Liked this? Get the next one.
No algorithms or news feeds. Add the RSS feed to your reader and new posts show up as soon as they're live.