Skip to content

Local-First GitHub Actions Strategy

gha

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 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.

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.

  1. Free Disk Space: My development container is a beast (around 18 GB), so the first step is necessary cleanup.
  2. Download Devcontainer: Pull the large, pre-built image that looks exactly like my devmachine.
  3. Clone the Repository: Standard checkout action.
  4. Run Tests Inside the Container: Execute the tests by using docker run and 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.

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.

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:

Terminal window
cat .github/workflows/ci.yml \
| jet -i yaml -o json \
| jq -r .jobs.test.steps[3].run \
| GITHUB_WORKSPACE=$(pwd) bash -s
  • cat .github/workflows/ci.yml: Reads the workflow file.
  • jet -i yaml -o json: Converts the YAML to JSON (I prefer using jet, but you could use other tools).
  • jq -r .jobs.test.steps[3].run: Extracts the exact run command 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 necessary GITHUB_WORKSPACE environment 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.

Terminal window
docker run
--rm
-u vscode
-e GITHUB_WORKSPACE=$GITHUB_WORKSPACE
-v $GITHUB_WORKSPACE:$GITHUB_WORKSPACE
ghcr.io/amiorin/big-container:latest
fish -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.

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.