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.
Why Smart CTOs Are Betting on Vietnam Outsourcing in 2025
TL;DR: Vietnam outsourcing is becoming the go-to strategy for CTOs who need elite engineering talent without Silicon Valley… ...
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.
Outsourcing Software Development in 2025: Why Vietnam Is Beating the Competition
TL;DR: Outsourcing software in 2025 isn’t about cutting costs anymore—it’s about accessing elite technical talent. Vietnam is now… ...
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.