I Automated 80% of My Open Source Maintenance with GitHub Actions — Here’s the Exact Setup
I maintain 12 open source repos. Not huge ones — think a few thousand stars each, a handful of active contributors, and a steady stream of issues and PRs.
For years, I did everything manually. Cut a release? Tag it, write changelog notes, update the README, publish to npm/PyPI. Dependency updates? Open Dependabot PRs, review, merge, repeat. Issue triage? Read every new issue, add labels, assign priorities.
How We Helped a Healthcare Startup Pass SOC 2 While Doubling Dev Output
How We Helped a Healthcare Startup Pass SOC 2 While Doubling Dev Output SOC 2 is a nightmare.… ...
It was killing my weekends. Honestly, it made me resent projects I once loved.
So I automated it. Not with some third-party SaaS tool. Just plain GitHub Actions, a few community actions, and some shell scripts. Here’s the exact stack I use across all my repos today.
Outsourcing Software Development: The Real-World Playbook for CTOs & Founders
TL;DR: Outsourcing software isn’t just about cost savings—it’s about accessing top-tier engineering talent globally. This article breaks down… ...
The Maintenance Tax Is Real
Let’s be blunt: open source maintenance is unpaid labor. You’re doing it because you care about the project, not because someone’s paying you. But when maintenance takes 5-10 hours a week per repo, that math stops working.
I’ve seen great projects die because the maintainer burned out. They didn’t abandon the code — they abandoned the *process*.
So I asked myself a simple question: *Which parts of this can a machine do better than me?*
Turns out, most of it.
My GitHub Actions Automation Stack
Here’s what I automated and the actions I use for each:
| Task | GitHub Action / Tool | Time Saved |
|---|---|---|
| Automated releases | `release-drafter/release-drafter` | ~45 min/release |
| Changelog generation | `release-drafter` + custom template | ~30 min/release |
| Dependency updates | `dependabot` + `auto-merge` | ~2 hrs/week |
| Issue triage | `actions/stale` + labeler | ~3 hrs/week |
| CI/CD testing | Standard `actions/checkout` + test runners | ~1 hr/release |
| Auto-merge PRs | `pascalgn/automerge-action` | ~1 hr/week |
Total: roughly 7-8 hours saved per week per repo. That’s a full working day. Every week.
1. Automated Releases with Release Drafter
This is the biggest time-saver. Instead of manually drafting release notes, I use `release-drafter`. It watches for PRs merged to `main` and automatically builds a changelog based on labels.
Here’s my `.github/release-drafter.yml`:
yaml
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
labels:
- 'chore'
- 'dependencies'
- 'refactor'
change-template: '- $TITLE (#$NUMBER) @$AUTHOR'
change-title-escapes: '\<*_&'
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
template: |
## What's Changed
$CHANGES
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
And the workflow that triggers it:
yaml
# .github/workflows/release-drafter.yml
name: Release Drafter
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize]
permissions:
contents: read
jobs:
update_release_draft:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
That's it. Every time a PR with the `feature` label merges, it goes under "Features". Bug fixes? Same thing. When I'm ready to ship, I just publish the draft. No copy-pasting, no forgetting to mention a contributor.
2. Changelog That Doesn't Suck
I used to hand-write changelogs. They were inconsistent. Sometimes I'd forget a PR. Sometimes I'd miss crediting a first-time contributor.
Release Drafter solves this, but I also added a custom template that includes a "New Contributors" section. That matters more than you think — first-time contributors are more likely to come back if they see their name in the release notes.
I added this to the template section:
yaml
template: |
## What's Changed
$CHANGES
## New Contributors
$CONTRIBUTORS
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
It automatically detects first-time contributors from the PR data. Zero effort on my part.
3. Dependency Updates That Merge Themselves
Dependabot is great, but it generates noise. Every week, 5-10 PRs for patch updates. Reviewing each one is a waste of time for low-risk dependencies.
I configured Dependabot to auto-merge patch and minor updates if CI passes. Here's the setup:
yaml
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "auto-merge"
And the auto-merge workflow:
yaml
# .github/workflows/auto-merge.yml
name: Auto-merge Dependencies
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
if: contains(github.event.pull_request.labels.*.name, 'auto-merge')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable auto-merge
run: |
gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Major updates still get manual review. But patch and minor? They merge automatically as long as tests pass. I've been running this for 8 months across 12 repos. Zero incidents.
4. Issue Triage That Never Sleeps
New issues come in at all hours. I'm in Ho Chi Minh City, but contributors are in San Francisco, Berlin, Tokyo. I can't be awake for all of them.
I use a labeler action that automatically categorizes issues based on keywords, plus a stale action that closes inactive issues after 60 days.
yaml
# .github/workflows/issue-triage.yml
name: Issue Triage
on:
issues:
types: [opened]
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const title = context.payload.issue.title.toLowerCase()
const body = context.payload.issue.body.toLowerCase()
if (title.includes('bug') || body.includes('reproduce')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
labels: ['bug', 'needs-triage']
})
} else if (title.includes('feature') || title.includes('request')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
labels: ['enhancement']
})
} else if (body.includes('how') || body.includes('question')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
labels: ['question']
})
}
And the stale action:
yaml
# .github/workflows/stale.yml
name: Close Stale Issues
on:
schedule:
- cron: '0 0 * * 1'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue has been inactive for 60 days. Please update or it will be closed.'
days-before-stale: 60
days-before-close: 14
stale-issue-label: 'stale'
exempt-issue-labels: 'bug,enhancement,priority'
Now, when I wake up in Ho Chi Minh City, issues are already labeled. I can focus on the ones that matter instead of sorting through the noise.
5. The CI/CD That Catches Everything
This one's standard, but I'll include it because it's the foundation. Every PR must pass linting, tests, and type checking before it can merge.
yaml
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --coverage
- uses: codecov/codecov-action@v4
Three Node versions, lint, type check, test, coverage. If any of these fail, the PR doesn't merge. Non-negotiable.
What I Still Do Manually
I'm not saying automation replaces everything. Here's what I still do by hand:
- Reviewing complex PRs — especially logic changes or architectural decisions
- Responding to nuanced issues — the ones that need context or design discussion
- Community management — thanking contributors, writing docs, fostering culture
- Roadmap decisions — what to build next, what to deprecate
But those are the *fun* parts of maintenance. The parts that actually need a human.
Why This Matters for Teams
Here's the thing: if you're running a remote team — say, you've hired developers in Vietnam through a platform like ECOAAI — this automation isn't just nice to have. It's how you scale without burning out your senior engineers.
I've seen teams where senior devs spend 30
Related: software outsourcing Vietnam — Learn more about how ECOA AI can help your team.
Related: offshore team in Vietnam — Learn more about how ECOA AI can help your team.
Related: Vietnam outsourcing — Learn more about how ECOA AI can help your team.
Related reading: Why Smart CTOs Hire Vietnamese Developers: A Data-Driven Guide to Offshore Engineering