Before You Push: Building a Local Twin for GitHub Actions
There is a particular kind of frustration every developer eventually learns.
You write the workflow.
You read it twice.
You check the indentation.
You push.
And then GitHub tells you that something was wrong all along.
Maybe an environment variable was missing.
Maybe an action was pinned incorrectly.
Maybe two jobs were never connected the way you thought they were.
Maybe a command worked perfectly on your machine and failed somewhere else.
And suddenly, the feedback loop becomes:
write
↓
push
↓
wait
↓
fail
↓
read logs
↓
change something
↓
push again
I wanted a different loop.
Not a fake GitHub Actions emulator that simply says everything is fine.
Something more honest.
Something that could take the workflow I had actually written, run the commands it could genuinely reproduce, isolate the jobs like separate runners, and tell me clearly when something could not be reproduced locally.
That idea became AeroCI.
A local twin for GitHub Actions
AeroCI is designed as a local twin for GitHub Actions.
The important word here is not local.
It is twin.
AeroCI attempts to reproduce the parts of a workflow that can actually be reproduced on your machine. It executes real commands in a real shell and provides the relevant GITHUB_* environment rather than pretending to simulate a successful run.
And when it cannot reproduce something?
It says so.
Not "success".
Not "probably fine".
Not a green checkmark hiding an assumption.
It says:
not simulated
That distinction is the foundation of AeroCI.
The first command is intentionally simple
You can start with:
npm install -g aeroci
cd your-project
aeroci init
aeroci check
aeroci run
init creates the local configuration and a sample workflow.
check validates the workflows against what the local runner actually requires.
And run executes them inside isolated job environments.
Interestingly, the configuration file is optional. AeroCI can run a project without .aeroci.json, using defaults where possible.
That was an important design choice.
A tool for developers should not create a new ceremony before it starts solving the problem.
The workflow should be treated as code
A workflow file may look like configuration, but it behaves much more like a program.
It has:
- dependencies
- execution order
- environments
- inputs
- outputs
- matrices
- shells
- external actions
- secrets
- network requirements
So AeroCI does more than execute it.
It can inspect it.
The check command validates things such as workflow structure, action pinning, secrets versus .env, the job graph, matrices, and shell availability.
Then analyze goes further.
It can identify dead steps, duplicated steps, unused outputs, the structure of the job graph, its longest chain, and a complexity score.
The goal is not merely:
"Did my workflow finish?"
It is also:
"What exactly did I build?"
That is a much more interesting question.
Isolation matters
One of the easiest ways for a local CI tool to become misleading is to let every job share the same project directory.
A job writes a file.
The next job sees it.
The test passes.
But that is not necessarily what would happen on separate CI runners.
AeroCI therefore gives every job its own copy of the project.
A matrix combination gets its own environment as well.
Your working tree remains untouched.
That small architectural decision changes the meaning of a local run.
You are no longer merely executing commands from a YAML file.
You are testing assumptions about isolation.
Secrets should not accidentally become files
There is another problem with reproducing CI locally:
secrets.
It would be convenient to simply copy .env into every sandbox.
It would also be a terrible default.
AeroCI deliberately leaves .env out of the working tree. Secrets are exposed to steps through the secrets context instead of being copied into the sandbox where an unrelated command could read them.
There is an opt-in exception for users who explicitly want excluded paths such as node_modules linked to their real directories.
And even there, the trade-off is made explicit:
if a step writes into those linked directories, it is writing into the real files.
The tool does not pretend otherwise.
The network is a separate decision
This was one of the principles I wanted AeroCI to make impossible to misunderstand:
AeroCI never uses the network without explicit permission.
But "network access" is not necessarily one decision.
There can be a difference between:
Downloading a runtime
and:
Allowing the workflow itself to open sockets
AeroCI therefore records these as separate policies.
Runtime downloads can require consent.
Workflow network access is denied by default.
The two permissions are intentionally independent.
And when the platform provides a mechanism to enforce the restriction, AeroCI actually enforces it rather than merely promising not to use the network.
On macOS it can use sandbox-exec.
On Linux it can use unshare --net.
So a command such as:
curl example.com
does not simply receive a polite warning.
It gets no network response.
At the same time, local communication such as a localhost server can continue to work.
That distinction matters.
A red result can be more useful than a green one
One of the easiest mistakes in CI tooling is to make unsupported things appear successful.
AeroCI takes the opposite approach.
Some GitHub Actions cannot honestly be reproduced locally.
Deployments.
Publishing.
Container builds.
CodeQL.
AWS OIDC role assumption.
Reusable workflow calls.
And actions for which there is no local implementation.
Instead of manufacturing confidence, AeroCI reports these as:
not simulated
The command may still be inspected or executed where possible, but the result explicitly tells you what was not verified.
That creates a useful rule:
A green local run should mean exactly what AeroCI was capable of verifying — and nothing more.
I think that is a healthier philosophy for developer tooling in general.
Debug the failure where it happened
Sometimes logs are not enough.
You know the step failed.
You know roughly where.
But you need to touch the environment yourself.
That is what:
aeroci debug
is for.
It opens a shell inside a copy of the project with the same CI environment, allowing you to rerun the failing command manually.
Instead of:
CI failed
↓
read log
↓
guess
you can move toward:
CI failed
↓
enter environment
↓
run command
↓
observe
↓
fix
That is a much shorter feedback loop.
CI should have a memory
A single workflow run tells you what happened.
A history can tell you what is changing.
AeroCI includes profiling and reporting tools for that reason.
profile keeps run history and trends against previous runs.
report can re-render a previous run, compare workflows, or show commits that touched them.
versions makes action pinning and simulation status visible.
There is also a read-only UI:
aeroci ui
Sometimes seeing a workflow as a graph is more useful than reading another hundred lines of YAML.
It is not GitHub Actions
And that distinction is important.
AeroCI does not attempt to replace GitHub Actions.
It does something narrower:
it gives your workflow a local place to be examined before you push it into a remote runner.
The remote system remains the final authority for things that inherently depend on the remote environment.
AeroCI's job is to reduce the number of things that have to be discovered there for the first time.
That is the difference between simulation and verification.
Why build this?
Because the distance between writing code and discovering that it is wrong should be as small as possible.
We have spent years shortening the feedback loop for application code.
Hot reload.
Local servers.
Unit tests.
Type checking.
Linters.
Formatters.
But CI configuration often remained strangely dependent on:
commit
→ push
→ wait
AeroCI is an attempt to bring another piece of that feedback loop home.
Not by pretending the remote environment does not exist.
By giving developers a local approximation that is explicit about its boundaries.
It can execute what it can execute.
It can inspect what it can inspect.
It can isolate what it can isolate.
And when it cannot reproduce something, it tells you.
That last part may be the most important feature of all.
The philosophy behind AeroCI
Developer tools are often judged by how much they can automate.
I think there is another question worth asking:
How honestly do they communicate what they automated?
A tool that says "passed" when it merely skipped the difficult part is convenient.
It is also dangerous.
A tool that says:
not simulated
is less magical.
But it is more trustworthy.
That is the direction I wanted AeroCI to take.
Not a fake GitHub Actions runner.
Not a promise that your laptop is GitHub.
A local twin with boundaries.
And perhaps that is the real purpose of developer tooling:
not to make reality disappear,
but to bring the useful parts of it closer.
Top comments (0)