Local-First GitHub Actions Strategy
If you’ve spent any significant time with GitHub Actions (GHA), you know the drill: it can be a massive time-saver, but when things go wrong, the development loop is painfully slow. Committing, pushing, waiting for the run to fail, and then repeating… it’s a productivity killer.
Over time, I’ve refined a strategy that cuts this frustrating cycle short. My philosophy is simple: Avoid any GitHub Actions feature that isn’t available or easy to replicate locally.
The Slow CI Development Loop
Section titled “The Slow CI Development Loop”The biggest headache with GitHub Actions isn’t writing the YAML; it’s debugging it.
When your build or test step fails, the quickest way to fix it is to reproduce the failure on your local machine. But if your CI heavily relies on GHA features, that local reproduction becomes difficult or impossible.
This leads to the dreaded “commit-and-wait” debugging cycle.
Docker-First CI Recipe
Section titled “Docker-First CI Recipe”My solution is to treat GitHub Actions purely as an execution environment for a set of simple, executable shell commands. Everything complex—the build, the test environment, the dependencies—is encapsulated within a Docker container.
My Typical CI Workflow Steps:
Section titled “My Typical CI Workflow Steps:”- Free Disk Space: My development container is a beast (around 18 GB), so the first step is necessary cleanup.
- Download Devcontainer: Pull the large, pre-built image that looks exactly like my devmachine.
- Clone the Repository: Standard checkout action.
- Run Tests Inside the Container: Execute the tests by using
docker runand mounting the repository volume.
I don’t write my own GitHub Actions. I rely only on standard shell commands and docker run. If I need something complex, I use battle-tested, popular actions written by others (like actions/checkout), but I don’t try to author my own actions.
Developing CI Without Committing
Section titled “Developing CI Without Committing”The real game-changer is the ability to run and debug the CI code locally without ever making a commit. This is the inner loop that makes CI development fast.
Since my CI logic is just a single step executed by GHA, I can extract and run that exact step on my machine.
The Local Debug Command
Section titled “The Local Debug Command”This single line of bash and command-line tools allows me to extract the critical run step from my workflow file and execute it locally:
cat .github/workflows/ci.yml \ | jet -i yaml -o json \ | jq -r .jobs.test.steps[3].run \ | GITHUB_WORKSPACE=$(pwd) bash -scat .github/workflows/ci.yml: Reads the workflow file.jet -i yaml -o json: Converts the YAML to JSON (I prefer usingjet, but you could use other tools).jq -r .jobs.test.steps[3].run: Extracts the exactruncommand script from the correct job and step.GITHUB_WORKSPACE=$(pwd) bash -s: Executes the extracted script in a new shell, mimicking how GHA works by defining the necessaryGITHUB_WORKSPACEenvironment variable and feeding the script in.
This technique is incredibly fast. I can iterate on my docker run command or environment variables until the script runs perfectly, all locally, and only then do I commit and push.
Example of CI step extracted with the singe line of bash above.
docker run--rm-u vscode-e GITHUB_WORKSPACE=$GITHUB_WORKSPACE-v $GITHUB_WORKSPACE:$GITHUB_WORKSPACEghcr.io/amiorin/big-container:latestfish -c 'cd $GITHUB_WORKSPACE&& mkdir ~/.ssh&& chmod 700 ~/.ssh&& ssh-keyscan -H github.com >> ~/.ssh/known_hosts&& git config --global user.name "github-actions[bot]"&& git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"&& bb build&& direnv allow&& direnv exec . just test'The Performance Trade-off and the Ultimate Fix
Section titled “The Performance Trade-off and the Ultimate Fix”My strategy has a performance “tax”:
- 5-10 minutes to free disk space.
- 5-10 minutes to download the 18 GB devcontainer image.
This is the unavoidable cost of a standardized, containerized environment on GitHub’s shared runners. If this 10-20 minute setup time is simply unacceptable for your project, there is one simple, permanent solution:
By using a self-hosted runner, your CI build machine is always ready. Your container is already pulled, and disk space is managed. Your CI job, once triggered, can jump straight to running tests in seconds, avoiding the entire pain of figuring out GitHub’s official caching architecture.
Conclusion
Section titled “Conclusion”GitHub Actions is a powerful tool, but its debugging experience can be maddening. By adopting a local-first, Docker-centric approach and reducing GHA to a simple execution engine, you can dramatically accelerate your CI development loop and reclaim your productivity.
Think locally, test locally, and only then commit globally. It’s the fastest path to a green build.
Would you like to have a follow-up on this topic? What are your thoughts? I’d love to hear your experiences.