Skip to content

How it works

waznggo edited this page Jul 26, 2026 · 1 revision

How it works

Five stages. Only the fourth touches Docker.

run URL
  │
  ├─ 1  RESOLVER      network only, runs nothing
  ├─ 2  STEP MAPPER   API steps → YAML step indices
  ├─ 3  SYNTHESIZER   pure function → a temporary workflow
  ├─ 4  EXECUTOR      act → container → your shell
  └─ 5  FIDELITY      every way local differs from CI

1. Resolver

Reads the run, the job, its steps, the commit, the runner labels and the event payload from the GitHub API, then fetches the workflow file at the run's commit — not at main, which may have moved on.

It deliberately executes nothing. That is what makes reenact inspect useful without Docker, keeps the whole thing testable against a recorded corpus with no network, and confines every risky operation to stage 4.

If the job calls another workflow (uses:), the called file is fetched too — from the run's own commit when the call is local (uses: ./…), or from the ref when it is not.

2. Step mapper

The API's step numbers are not YAML step indices. Measured across 243 real jobs, 92% of them have numbers that skip, plus steps the runner injects that exist in no file:

1  Set up job          ← synthetic
2  Run git config …
5  Install Go
7  golangci-lint       ← the failure
11 Post golangci-lint  ← 8,9,10 missing, and synthetic

So synthetic steps are dropped and the rest are matched by name. Where a name contains an expression, the matrix leg is substituted first and the rest is matched as a pattern. Where a name resolves to nothing but an expression — a pattern that would match anything — the mapping is refused rather than guessed.

3. Synthesizer

Produces a temporary workflow containing only the failing job, with:

  • the matrix pinned to the failing leg (not deleted — deleting it would make every ${{ matrix.* }} resolve empty)
  • steps truncated at the breakpoint, plus a final step that holds the container open
  • steps that were skipped in the real run left out entirely, and the if: of steps that did run removed — the run already told us the answer, and re-evaluating the expression locally can produce a different one
  • needs and job-level if dropped, each reported in the fidelity report

The YAML is edited surgically rather than rewritten, so keys reenact does not model — services, defaults, container, env — survive untouched.

4. Executor

Clones the commit into a temporary directory (never your working tree), runs act with --bind so .git actually reaches the container, waits for the hold step's token file, then docker execs you into it. On exit everything is removed, including files the container wrote as root.

5. Fidelity report

See The fidelity report.

Clone this wiki locally