I ditched GitHub Actions for a 50-line Makefile. Here’s why my 12 open-source projects are better off.

1 comment
(GitHub and Open Source) - Spending more time wrestling your CI/CD config than shipping features? We moved 12 repos off complex GitHub Actions workflows to a single, shared Makefile. The result: 90% faster onboarding, zero CI drift, and a happier maintainer. Here's the exact setup.

I ditched GitHub Actions for a 50-line Makefile. Here’s why my 12 open-source projects are better off.

Let me paint you a picture.

You’re maintaining 12 open-source repos. Each one has its own GitHub Actions workflow — some are clean, some are Frankensteins of copy-pasted jobs from Stack Overflow. One project needs Python 3.9, another needs 3.12. Your linter config is a mess. Your test matrix runs 14 variations across 6 OSes, and it takes 45 minutes to get a green checkmark.

Why Smart CTOs Hire Vietnamese Developers: The 2025 Offshore Advantage

Why Smart CTOs Hire Vietnamese Developers: The 2025 Offshore Advantage

TL;DR: Vietnam is emerging as the top destination for offshore software development in 2025. With a 95% developer… ...

I was there. And I was burned out.

So I ripped it all out.

Build a Custom AI Code Review Agent: A Step-by-Step Tutorial with ECOA AI Platform ACP

Build a Custom AI Code Review Agent: A Step-by-Step Tutorial with ECOA AI Platform ACP

Build a Custom AI Code Review Agent: A Step-by-Step Tutorial with ECOA AI Platform ACP Let’s be honest.… ...

I replaced 12 bloated GitHub Actions workflows with a single, 50-line `Makefile` that lives in a shared shell script repo. The results were immediate. Onboarding new contributors went from “please wait 30 minutes for CI to tell you your import is wrong” to “run `make lint` locally in 3 seconds.” Our CI drift — where the pipeline and the local dev environment disagree — dropped to zero.

Honestly? I should have done this years ago.

The Real Problem: CI Should Not Be Black Magic

Here’s the dirty secret nobody talks about: Most open-source CI pipelines are lying to you.

They pass on your pull request, but when you run `pytest` locally, you get a totally different error. Why? Because your `Dockerfile` uses Ubuntu 22.04, but GitHub Actions defaults to `ubuntu-latest` which is now 24.04. Or your CI installs packages with `pip install .` but your local `.venv` uses `pip install -e .`.

That’s not a test failure. That’s a trust failure.

A single, version-controlled `Makefile` solves this. It explicitly codifies exactly how to lint, test, build, and deploy. No hidden environment variables. No implicit Docker images. No “it works on my machine” excuses. Just a crisp, human-readable set of commands.

More importantly, your contributors can run it locally without ever touching GitHub.

The Makefile Pattern That Replaced My GitHub Actions

Here’s the exact `Makefile` pattern I use. It’s generic enough to drop into any Python or Node project.

makefile
.PHONY: help lint test build clean setup

# Default target shows help
help:
	@echo "Available targets:"
	@echo "  setup    - Install dependencies"
	@echo "  lint     - Run linters (ruff + pre-commit)"
	@echo "  test     - Run unit tests with coverage"
	@echo "  build    - Build Python wheel / Docker image"
	@echo "  clean    - Remove build artifacts and .venv"

setup:
	pip install --upgrade pip
	pip install -e ".[dev]"
	pre-commit install

lint:
	ruff check .
	ruff format --check .
	pre-commit run --all-files

test:
	python -m pytest tests/ -v --cov --cov-report=term-missing

build:
	python -m build

clean:
	rm -rf dist/ build/ *.egg-info
	rm -rf .venv
	find . -type d -name __pycache__ -exec rm -rf {} +

That’s it. 33 lines. No YAML. No Docker. No magic.

Then, the corresponding GitHub Actions workflow becomes a thin wrapper:

yaml
name: CI
on: [push, pull_request]
jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: make setup
      - run: make lint
      - run: make test

This workflow is identical across all 12 repos. When I update the `Makefile`, I push it to a central scripts repo, then run a quick `git submodule update` (or a small shell script to copy it) across every project.

The real win? I know this workflow works. It’s the same commands I run 20 times a day locally. If it fails in CI, it fails for a real reason, not a CI-configuration phantom.

The Metrics That Matter

I pulled data across my 12 repos over the 6 months after the migration.

Metric Before (GitHub Actions) After (Makefile)
CI pipeline definition ~120 lines YAML per repo 14 lines YAML + 33 lines Makefile
Avg. CI time (green) 24 min 11 min
Local dev parity (pass/fail) 62% match 98% match
New contributor FPS (~1st PR time) 3.4 hours 37 minutes
Pre-commit + lint failures Happened in CI Caught on commit

The contributor onboarding data was the killer. Previously, a new developer would fork the repo, read the `README`, run what they thought was the setup, push, wait 20 minutes… and get a lint failure. Now, they run `make lint` locally. It fails, they fix it. Then they push. Green checkmark.

It’s not just faster. It’s a better developer experience.

The Geo-Specific Advantage: Why This Matters for Vietnam-Based Teams

I’m writing this from Ho Chi Minh City, where our ECOA AI engineering hubs are based. We’ve onboarded over 40 developers across our Can Tho and HCMC offices onto this exact workflow.

Why does this matter?

Bandwidth. Many Vietnamese devs still work with variable internet connections. If your CI pipeline requires a 500MB Docker pull for every run, you’re destroying their productivity. A `Makefile` + `venv` setup runs offline. One engineer in Can Tho told me last week, *”I can now run the entire test suite on the bus using my 4G hotspot. Before, I had to wait until I got to the office.”*

That’s not just a comfort win. That’s a real velocity gain.

When to Stick with GitHub Actions

To be fair, I’m not saying all CI should be a `Makefile`.

If you’re building a multi-architecture project (think: ARM + x86 Docker images), or you need heavy parallelization across 20 different permutations, GitHub Actions matrix builds are still the right call. The `Makefile` can’t spin up 20 parallel Linux runners.

But for 80% of open-source Python and Node projects? The `Makefile` pattern is strictly better.

Here’s my rule of thumb: If your CI pipeline does more than 3 things (lint, test, build), and those steps aren’t trivially reproducible locally, you’ve already lost.

How to Start Your Migration

It’s easier than you think. Here’s the playbook:

  1. Extract your local dev commands from your `README` into a `Makefile`. Start with `setup`, `lint`, `test`.
  2. Pin your Python/Node version in a `.python-version` or `.nvmrc` file. Your `Makefile` should read from it, not hardcode the version.
  3. Simplify your GitHub Actions workflow to 3 steps: checkout, setup, `make lint && make test`.
  4. Test the parity. Run `make test` locally. Then push to a PR branch. If they don’t match exactly, fix the `Makefile` — not the CI.

I did this for one repo in an afternoon. It took me longer to write this blog post than to migrate the first project.

The Bigger Picture

Your open-source project isn’t a masterclass in YAML configuration. It’s a piece of software that solves a problem. Every minute you spend debugging a CI pipeline is a minute you’re not shipping features, fixing bugs, or helping contributors.

The `Makefile` isn’t a silver bullet. It’s a philosophy: CI should be the same as local development, codified cleanly.

Stop maintaining 12 copies of the same fragile CI pipeline. Write one `Makefile`. Let your developers — whether they’re in San Francisco, Ho Chi Minh City, or a coffee shop in Can Tho — just run `make`.

Frequently Asked Questions

Why not just use Makefiles inside GitHub Actions directly?

You should. That’s exactly what I’m advocating for. Instead of putting complex logic in YAML (which is hard to test and parse), push the logic into a `Makefile`. Then call it from a simple YAML wrapper. Your CI becomes shorter, and developers can run the same targets offline.

Won’t this break for contributors on Windows?

Yes, `make` isn’t native to Windows. But WSL2 is ubiquitous now. I add a brief note in the `CONTRIBUTING.md`: “This project uses `make`. If you’re on Windows, please use WSL2 or open a PR to add a `make.bat` equivalent.” In practice, 95% of contributors use macOS or Linux anyway. For the rest, `make` is a one-time install via `choco install make`.

How do you handle project-specific env variables in a shared Makefile?

I use a `.env.sample` file and a `Makefile` target: `include .env`. The `.env` is in `.gitignore`. The `Makefile` reads it with `include .env`. Each project gets its own `.env`, but the `Makefile` targets remain generic. This keeps the shared workflow intact without leaking secrets.

Does ECOA AI’s teams use this workflow with AI agent orchestration?

Yes. Our developers in Vietnam use this `Makefile`-first CI pattern paired with the ECOA AI Platform (ACP). When an AI agent generates code, it first runs `make lint` locally inside a sandboxed environment. If it fails, the agent fixes it *before* the code ever hits a pull request. This pre-commit AI validation loop has cut our bad code generation rate by over 52%. We’ve shared the exact ACP agent configuration with our clients — ping us if you want to see it.

Related reading: Why Vietnam Outsourcing Is Now the Smartest Bet for Your Engineering Team

Related reading: Outsourcing Software in 2025: The Strategic Playbook for CTOs and Startup Founders

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.