How We Built an Automated Open Source Newsletter with GitHub Actions and Python (And Grew Our Community 3x)

1 comment
(GitHub and Open Source) - Most open source maintainers struggle with community engagement. We automated weekly curation and grew engagement 3x. Here's the exact workflow with Python, GitHub Actions, and a Vietnamese team.

How We Built an Automated Open Source Newsletter with GitHub Actions and Python (And Grew Our Community 3x)

Running an open source project isn’t just about code. It’s about people. But keeping them in the loop without burning out? That’s the real challenge.

We manage several open source tools over at ECOA AI, and for months our community was growing faster than our ability to communicate. We’d release features, fixes, and breaking changes—but many contributors only found out weeks later. Engagement flatlined.

Why Smart CTOs Hire Vietnamese Developers: A Data-Driven Guide to Offshore Engineering

Why Smart CTOs Hire Vietnamese Developers: A Data-Driven Guide to Offshore Engineering

TL;DR: Vietnam is now the top destination for offshore software development. You get strong technical skills (especially in… ...

So we built something stupid simple: a weekly newsletter generated entirely by a Python script, triggered by GitHub Actions, and deployed to Mailchimp via API.

The result? A 3x increase in active contributors over two months and a pile of sanity saved for the maintainers.

Vietnam Outsourcing: The Silent Revolution in Offshore Software Development

Vietnam Outsourcing: The Silent Revolution in Offshore Software Development

TL;DR: Vietnam outsourcing is rapidly becoming the go‑to destination for companies wanting quality software development at a fraction… ...

Here’s the exact setup, the code, and the lessons we learned from our team in Can Tho, Vietnam.

Why Not Just Use a Marketing Tool?

Honestly, most newsletter tools are overkill for open source. You don’t need landing pages or A/B testing. You need to automate the curation of what’s happened in your repo in the last week: merged PRs, new issues, releases, milestones.

Manual curation takes 30-60 minutes per week. Multiply that by 12 projects—it’s a full-time job. We wanted zero human involvement, except for a quick review before sending.

So we built a Python script that pulls from the GitHub API and assembles a Markdown newsletter. Then GitHub Actions runs it every Monday at 9 AM UTC, posts the draft as a comment in a private “newsletter-queue” issue, and an admin just clicks “Send” via a custom Slack command.

Here’s the architecture:


[GitHub Actions (schedule)] -> [Python script] -> [Generate MD file] -> [Create issue in #newsletter-queue] -> [Slack bot notifies admin] -> [Admin approves -> script sends via Mailchimp API]

No, we don’t use a full CI/CD pipeline for sending emails. That’s over-engineering. The approval step is just a human eyeball.

The Core Python Script (Simplified)

Let’s be real—the beauty is in the simplicity. Here’s the skeleton:

python
import requests
import os
from datetime import datetime, timedelta

GITHUB_REPO = "ecoaai/ecoa-platform"
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")

def get_merged_prs(period_days=7):
    since = (datetime.utcnow() - timedelta(days=period_days)).isoformat() + "Z"
    url = f"https://api.github.com/repos/{GITHUB_REPO}/pulls"
    params = {"state": "closed", "sort": "updated", "direction": "desc", "since": since}
    headers = {"Authorization": f"token {GITHUB_TOKEN}"}
    resp = requests.get(url, headers=headers, params=params)
    # Filter for merged PRs within the window
    merged = [pr for pr in resp.json() if pr.get("merged_at")]
    return [{"title": pr["title"], "url": pr["html_url"], "author": pr["user"]["login"]} for pr in merged[:10]]

def build_newsletter(prs, releases, issues):
    lines = []
    lines.append(f"# {GITHUB_REPO} Weekly Digest")
    lines.append(f"**Week of {datetime.utcnow().strftime('%B %d, %Y')}**\n")
    if releases:
        lines.append("## 🚀 Releases")
        for r in releases:
            lines.append(f"- [{r['name']}]({r['url']})")
        lines.append("")
    if prs:
        lines.append("## 🔀 Merged PRs")
        for p in prs:
            lines.append(f"- [{p['title']}]({p['url']}) by @{p['author']}")
        lines.append("")
    # ... same for new issues, milestones, etc.
    return "\n".join(lines)

if __name__ == "__main__":
    prs = get_merged_prs()
    # ... call API for releases, issues, etc.
    md = build_newsletter(prs, [], [])
    # Write to file
    with open("newsletter_output.md", "w") as f:
        f.write(md)
    print("Newsletter generated.")

Notice we keep it in Markdown. That’s deliberate. We then convert to HTML with `markdown2` before piping to Mailchimp’s API. But the admin reviews the MD first.

The GitHub Actions Workflow

We use a scheduled workflow with a `workflow_dispatch` manual trigger for emergencies.

yaml
name: Generate Weekly Newsletter
on:
  schedule:
    # Every Monday at 09:00 UTC
    - cron: '0 9 * * 1'
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install requests markdown2 mailchimp-marketing
      - name: Run newsletter generator
        run: python newsletter_generator.py
        env:
          GITHUB_TOKEN: ${{ secrets.GH_NEWSLETTER_TOKEN }}
          MAILCHIMP_API_KEY: ${{ secrets.MAILCHIMP_API_KEY }}
          MAILCHIMP_LIST_ID: ${{ secrets.MAILCHIMP_LIST_ID }}
      - name: Create Issue with Newsletter Draft
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const md = fs.readFileSync('newsletter_output.md', 'utf8');
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `Newsletter Draft - ${new Date().toISOString().slice(0,10)}`,
              body: md,
              labels: ['newsletter', 'automated']
            });

That’s it. No email sending until a human approves. We learned the hard way that auto-sending leads to disasters (more on that later).

How Our Vietnamese Team Made This Production-Ready

I’ll be direct: the initial prototype was my weekend hack. But to make it robust—error handling, rate limiting, graceful failure when GitHub API changes—we assigned it to a mid-level developer in Can Tho, one of our offshore engineering hubs.

She spent three days polishing it. She added:

  • Pagination for repos with 100+ PRs in a week.
  • Rate-limit detection with exponential backoff.
  • A dry-run mode that posts to a test Mailchimp list.
  • A fallback template when no changes occurred (so subscribers don’t get empty emails).

We paid around $200 for that engineering time. A full-time content marketer would charge $500/week for the same curation. The math is brutal.

The Results After 3 Months

Metric Before (manual) After (automated)
Hours/week spent on newsletter 2–3 0.1 (review only)
Average open rate 22% 34%
New contributors/month 12 38
Newsletter typos 7 total in 3 months 0 (template is fixed)

More importantly, contributors started looking forward to Monday morning emails. They knew exactly what changed and could jump in.

But we almost blew it. In week 2, a bug accidentally included a stale PR from three months ago. The “merged_at” filter was off by one day due to UTC off-by-one. That’s why we keep the human check.

3 Lessons for Open Source Maintainers

1. Don’t send without a human gate. A broken newsletter erodes trust faster than no newsletter.

2. Leverage contributors for content. We added a section for “Community Highlights” that we pull from GitHub Discussions’ most-liked posts. It made people feel seen.

3. Make it cheap to maintain. The entire pipeline costs $0 in infrastructure (GitHub free tier + Mailchimp free tier up to 2k subscribers). The only cost is the human review.

Frequently Asked Questions

Q: Why not use an existing tool like Revue or Substack?

A: Those require manual curation. Our goal was zero human effort for the generation phase. Plus, we wanted total control over the template—we use Markdown for easy review, which most tools don’t support natively.

Q: How do you handle unsubscribes from GitHub API?

A: We don’t. The newsletter is opt-in via a link in the repo’s README that goes to a Mailchimp signup. Unsubscribes are handled by Mailchimp’s standard footer link. We don’t automatically add contributors to the list.

Q: Can I adapt this for multiple repos?

A: Yes, but you’ll need to loop over an array of repos in the Python script. Be mindful of GitHub API rate limits (5000 requests/hour). With 10 repos and running once a week, you’re fine.

Q: Our community is mostly on Discord—should we still do email?

A: Email has higher engagement for long-form content. Use Discord for real-time chatter, email for weekly summaries. We saw a 40% click-through to the GitHub releases page from email, vs 5% from Discord announcement posts.

Related reading: Outsourcing Software Development: The Playbook for Building High-Performance Remote Teams in 2025

Related reading: Why We Bet the Farm on Vietnam: The Smartest Move to Hire Vietnamese Developers in 2025

Leave a Comment

Your email address will not be published. Required fields are marked *

Ready to Build with AI-Powered Developers?

Hire Vietnamese engineers augmented by ECOA AI Platform + Claude Code. 5x faster, 40% cheaper.