My Journey from GitHub Actions Back to Makefiles
Why did I delete all my GitHub Actions and return to 50-year-old Makefiles? The surprising journey from cloud dependency back to local control, and how it changed everything.
Problem
I became a slave to platforms. GitHub Actions, CI/CD pipelines, cloud services—they all promise convenience but deliver dependency. You push code, wait for workflows, hope they succeed. You're not in control; the platform is.
The Awakening
Today, I deleted .github/workflows/
. All of it. Gone.
Why? Because I realized something fundamental: Platforms are just interfaces. Logic should be mine.
The Makefile Renaissance
What I Had (Platform Slavery)
# .github/workflows/publish.yml
name: Publish Packages
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm publish
Problems:
Vendor lock-in to GitHub
Debugging requires pushing code
Waiting for runners
Limited to GitHub's environment
No local testing
What I Built (Total Control)
# Makefile
publish:
@for dir in $$(fd package.json packages/ -x dirname {}); do \
pkg=$$(jq -r '.name' $$dir/package.json); \
version=$$(jq -r '.version' $$dir/package.json); \
published=$$(npm view $$pkg version 2>/dev/null || echo "0.0.0"); \
if [ "$$version" != "$$published" ]; then \
echo "Publishing $$pkg@$$version..."; \
(cd $$dir && npm publish); \
fi \
done
Benefits:
Works anywhere (local, CI, anywhere with make)
Instant execution
Full transparency
No external dependencies
Complete control
The Philosophy
This connects to my core principle: Zero Documentation -> Living Code
Now extended: Zero Platform Dependency -> Total Control
Before: Interface-Driven Development
Developer -> GitHub -> Actions -> npm
↑ ↑ ↑
Interface Platform Service
Each arrow is a dependency. Each dependency is a potential failure point.
After: Logic-First Development
Developer -> Makefile -> Direct Execution
↑ ↑
Logic Control
No intermediaries. No waiting. No surprises.
Real Implementation
Here's my actual Makefile that replaced all GitHub Actions:
.PHONY: help readme publish publish-all list clean test
# Generate README dynamically from packages
readme:
@echo "# vibe-coding" > README.md
@echo "" >> README.md
@echo "| Command | Package | Description |" >> README.md
@echo "|---------|---------|-------------|" >> README.md
@for dir in $$(fd package.json packages/ -x dirname {} | sort); do \
name=$$(basename $$dir); \
pkg=$$(jq -r '.name' $$dir/package.json); \
desc=$$(jq -r '.description' $$dir/package.json); \
echo "| [\`$$name\`](packages/$$name) | \`$$pkg\` | $$desc |" >> README.md; \
done
# Version bump all packages
bump:
@for dir in $$(fd package.json packages/ -x dirname {}); do \
pkg=$$(jq -r '.name' $$dir/package.json); \
old=$$(jq -r '.version' $$dir/package.json); \
new=$$(echo $$old | awk -F. '{print $$1"."$$2"."$$3+1}'); \
jq ".version = \"$$new\"" $$dir/package.json > $$dir/package.json.tmp && \
mv $$dir/package.json.tmp $$dir/package.json; \
echo "$$pkg: $$old -> $$new"; \
done
# Test all tools
test:
@for dir in $$(fd package.json packages/ -x dirname {}); do \
name=$$(basename $$dir); \
echo "Testing $$name..."; \
(cd $$dir && bun run ./index.ts -h) || true; \
done
The Power of Make
Make is 48 years old (1976). It survived because it follows Unix philosophy:
Do one thing well
Everything is a file
Compose simple tools
Text is the universal interface
GitHub Actions is 5 years old. It won't survive 48 years. Make will.
Practical Benefits
1. Local-First Development
make test # Test locally
make publish # Publish locally
# No pushing to GitHub to test workflows
2. Platform Independence
Works on:
Local machine
Any CI/CD platform
SSH servers
Docker containers
Anywhere with
make
3. Speed
GitHub Actions: Push -> Wait for runner -> Execute -> Results (2-5 minutes)
Makefile: Execute -> Results (instant)
4. Transparency
make -n publish # See what will run without executing
You can't do this with GitHub Actions.
The Broader Pattern
This is part of a larger realization:
Platforms are interfaces -> Control the logic
Services are conveniences -> Own the capability
Clouds are computers -> Your computer is enough
Workflows are just scripts -> Write scripts
Migration Guide
Step 1: Identify Platform Dependencies
GitHub Actions -> Makefile
Vercel -> Static hosting
AWS Lambda -> Local server
Heroku -> Systemd service
Step 2: Extract the Logic
What does the platform actually do? Write it yourself.
Step 3: Create Local-First Tools
Every cloud service can be replaced with:
A script
A Makefile target
A local service
A simple binary
Real-World Examples
Dynamic README Generation
Instead of a static README that gets outdated:
readme:
@for package in packages/*; do \
echo "- [$$(basename $$package)]($$package)" >> README.md; \
done
Auto-Publishing
Instead of GitHub Actions:
publish: readme
@git diff --exit-code || (git add -A && git commit -m "auto: update" && git push)
@make publish-npm
Continuous Testing
Instead of CI/CD:
watch:
@fswatch -o . | xargs -n1 -I{} make test
The Unix Way Wins
I returned to basics:
Make instead of CI/CD platforms
Shell scripts instead of workflows
Git hooks instead of GitHub Apps
Cron instead of scheduled actions
Each tool does one thing well. Each tool is under my control.
Conclusion
Deleting .github/workflows/
was liberating. I'm no longer waiting for runners, debugging YAML, or tied to GitHub's infrastructure.
The Makefile is my declaration of independence. It's code I control, logic I own, and automation that works everywhere.
Stop configuring platforms. Start writing Makefiles.
The best workflow is no workflow—just make.
Part of the Zero Documentation -> Living Code philosophy. See also: @yemreak/culture - my tool for discovering patterns from code instead of reading documentation.
Last updated
Was this helpful?