I Automated 80% of My Open Source Maintenance with GitHub Actions — Here’s the Exact Setup
Let’s be real. Maintaining a popular open source project is a second job you don’t get paid for.
I ran a moderately popular React component library for three years. It had about 4,500 stars, 200+ open issues at peak, and a steady stream of PRs. I loved the community. I hated the *maintenance*. The triage, the stale PRs, the “bump version” requests, the dependency alerts at 2 AM.
Your Multi-Agent Orchestrator Is a Serial Killer: Why Parallel Execution Is the Only Way to Scale (And How to Build It)
Your Multi-Agent Orchestrator Is a Serial Killer: Why Parallel Execution Is the Only Way to Scale (And How… ...
I was burning out. Fast.
So I did what any lazy engineer would do: I automated the hell out of it. I built a GitHub Actions pipeline that now handles roughly 80% of my maintenance workload. It’s not magic. It’s just a few well-crafted workflows that run on cron, on push, and on issue/PR events.
Hire Vietnamese Developers: The Strategic Edge for Scalable Tech Teams
TL;DR: Vietnam is now the top offshoring destination for startups and enterprises. With 500k+ developers, strong English skills,… ...
Here’s the exact setup. Steal it.
The Three Pillars of Open Source Automation
You don’t need to automate everything. You need to automate the *grind*. I focused on three areas:
- Issue & PR Triage – Labeling, stale detection, and closing abandoned threads.
- Dependency Management – Automated updates with sensible grouping and testing.
- Release Engineering – Changelog generation, version bumping, and publishing to npm.
Each of these is a separate workflow file in `.github/workflows/`. Let’s break them down.
1. Automated Issue & PR Triage
This is the biggest time sink. Reading every issue, asking for reproduction steps, closing the ones that went cold. I wrote a workflow that runs twice a day.
yaml
# .github/workflows/triage.yml
name: Triage Issues and PRs
on:
schedule:
- cron: '0 8,20 * * *' # Runs at 8 AM and 8 PM UTC daily
workflow_dispatch: # Allow manual trigger
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been inactive for 30 days. Please provide an update or it will be closed in 7 days.'
stale-pr-message: 'This PR has been inactive for 21 days. Please address review comments or it will be closed in 7 days.'
close-issue-message: 'Closing due to inactivity. Feel free to reopen if the issue persists.'
close-pr-message: 'Closing due to inactivity. Please open a new PR with the latest changes.'
days-before-stale: 30
days-before-close: 7
days-before-pr-stale: 21
days-before-pr-close: 7
stale-issue-label: 'stale'
stale-pr-label: 'stale'
exempt-issue-labels: 'bug,enhancement,help wanted'
exempt-pr-labels: 'dependencies,automerge'
operations-per-run: 100
Why this works: It’s aggressive. 30 days of silence on an issue? It gets the stale label. 7 more days? Closed. But I exempt `bug` and `enhancement` labels because those are real work. The `help wanted` label stays open forever—that’s the community’s todo list.
Honestly, the hardest part was convincing myself to close things. But a closed issue is better than a zombie issue that wastes everyone’s time.
2. Dependency Updates That Don’t Break Everything
Dependabot is fine for security patches. But for a library? It creates noise. I switched to Renovate Bot (and wrote about why here). But the real magic is in how I *test* those updates.
yaml
# .github/workflows/dependency-update-test.yml
name: Test Dependency Updates
on:
pull_request:
branches: [main]
paths:
- 'package.json'
- 'pnpm-lock.yaml'
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test -- --coverage
- run: pnpm build
The key insight: I only run this on PRs that touch `package.json` or the lockfile. That’s it. No point running a full test suite on a documentation PR. This cut my CI costs by about 40%.
But here’s the real pro tip: I use Renovate’s `groupName` config to batch minor and patch updates into a single PR. My `renovate.json` looks like this:
json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch",
"automerge": true,
"platformAutomerge": true
},
{
"matchUpdateTypes": ["major"],
"labels": ["major-update"],
"automerge": false
}
]
}
Minor and patch updates get auto-merged after the test workflow passes. Major updates? Those get a label and sit in the queue for manual review. I’ve been running this for 8 months. Zero regressions from auto-merged updates.
3. Release Engineering: From PR to npm in 5 Minutes
This was the game changer. Before automation, a release meant:
- Manually bumping the version in `package.json`.
- Writing a changelog entry.
- Creating a GitHub Release.
- Running `npm publish`.
- Tagging the commit.
That’s 15 minutes of boring, error-prone work. Now it’s a single label.
yaml
# .github/workflows/release.yml
name: Release
on:
pull_request:
types: [closed]
branches: [main]
jobs:
release:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Bump version and generate changelog
id: version
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
release_branches: main
default_bump: patch
- name: Publish to npm
run: pnpm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.new_tag }}
name: Release ${{ steps.version.outputs.new_tag }}
body: ${{ steps.version.outputs.changelog }}
generate_release_notes: true
The workflow is simple:
- A PR gets merged that has the `release` label.
- The action bumps the version (defaults to patch, but you can use `#major` or `#minor` in the PR title to override).
- It generates a changelog from conventional commits.
- It publishes to npm with provenance (trust me, this matters for security).
- It creates a GitHub Release with the changelog.
I’ve cut my release time from 15 minutes to about 30 seconds. And I never forget to tag the commit anymore.
What I Didn’t Automate (And Why)
Not everything should be automated. I still manually handle:
- Security vulnerability discussions – These need human judgment.
- Feature request prioritization – The community needs to feel heard.
- Code review for complex PRs – AI can help, but I still read every line of a non-trivial PR.
But the boring stuff? The triage, the updates, the releases? That’s all on autopilot now.
The Real Numbers
After 6 months with this setup:
- Open issues dropped from 200+ to ~45. Most of those are legitimate bugs or feature requests.
- Average time to first response on issues: 4 hours (down from 3 days).
- Dependency updates applied within 24 hours of release.
- Release cycle: 1-2 weeks (down from 4-6 weeks).
I spend about 2 hours per week on maintenance now. Down from 10-15. That’s an 80% reduction.
The Vietnam Connection
You might be wondering why a post about GitHub Actions is on a site about Vietnamese developers. Here’s the thing: I built this pipeline while working with a team in Ho Chi Minh City. We were building a SaaS product, and I was maintaining the open source library on the side.
My Vietnamese colleagues—senior engineers at ECOA AI—showed me a few tricks. Like using `pnpm` instead of `npm` for faster installs in CI. Or the `mathieudutour/github-tag-action` for semantic versioning. They’re not just great coders; they’re pragmatic about automation. They hate repetitive work as much as I do.
If you’re a maintainer burning out, you have two options: hire help (and Vietnam is the best value for that right now) or automate. I did both. The automation came first.
Frequently Asked Questions
Q: Won’t auto-merging dependency updates break my project?
A: Only if you don’t have good test coverage. I run tests on three Node versions (18, 20, 22) before any merge. If your test suite is solid, minor and patch updates are safe to auto-merge. Major updates still need manual review.
Q: How do I handle breaking changes in dependencies?
A: Renovate’s `major` update rules are your friend. I label major updates and review them manually. I also use `npm-check-updates` locally to preview breaking changes before merging.
Q: Can I use this setup for a private repository?
A: Absolutely. GitHub Actions works the same for private repos. Just make sure your `NPM_TOKEN` or other registry tokens are stored as repository secrets. The `stale` action works on private repos too.
Q: What if I don’t use pnpm?
A: Swap `pnpm/action-setup` for the npm or yarn equivalent. The logic is identical. I use pnpm because it’s faster and has better disk usage in CI, but the workflow structure doesn’t change.