Skip to content

ci: gate worker-mode state leaks with Igor - #80

Merged
turegjorup merged 1 commit into
developfrom
ci/igor-worker-mode-gate
Aug 26, 2026
Merged

ci: gate worker-mode state leaks with Igor#80
turegjorup merged 1 commit into
developfrom
ci/igor-worker-mode-gate

Conversation

@turegjorup

Copy link
Copy Markdown
Contributor

Adds Igor as a CI gate for the worker-mode contract documented in #79.

Changes

  • igor-php/igor-php as a dev dependency, run through the existing phpfpm container like every other tool here.
  • igor.json — scopes the scan to the bundle's own code and points at the baseline.
  • igor-baseline.json — the four values this bundle shares on purpose, each with a written reason.
  • task analyze:worker, analyze:worker:check, analyze:worker:baseline; analyze:worker joins pr:actions.
  • A Worker mode (Igor) CI job.
  • README section on running and maintaining it; changelog entry.

What it actually gates

Igor reports shared mutable state, which is not the same thing as a leak. Every finding here is deliberate:

Finding Why it is in the baseline
$httpClients Guzzle clients: fixed options plus a connection pool. Sharing them is the point.
$redirectUriPaths Paths derived from config and the routing base URL, keyed by that base URL. Two requests sharing a key derive identical values.
$logger Written once by the container at build time.
Bundle::getContainerExtension() Symfony's own lazy init, at container compile time.

So the gate is not "is there shared state" — the answer is yes, four times, on purpose. It is "has shared state appeared that nobody has justified in writing". The baseline format carries a reason field per entry and ships with TODO: Explain why..., so an unjustified addition is visible in review as well as failing the build.

That case is worth catching. The provider memoization removed in #77 was exactly this shape — mutable state on a shared service, harmless until a process outlives a request — and nothing but review would have stopped it coming back.

Verified

$ task analyze:worker
✅ OK (Stateless): 24    ❌ KO: 0
✨ CONGRATULATIONS: Your application ... compatible with Worker Mode!   exit 0

Then with a property mutation planted in CliLoginHelper:

❌ KO (Dangerous State): 1 (Project: 1)     exit 201

Reverted, back to exit 0. analyze:worker:check also passes, confirming no baseline entry has gone stale.

Decisions worth reviewing

  • Pinned to 0.9.5, in two places. Igor is pre-1.0 and its rules change between releases, which would move the baseline underneath us. IGOR_VERSION is set in Taskfile.yml and .github/workflows/php.yaml; they have to be bumped together. I considered a single source but every other version in this repo is pinned per-consumer too, and a workflow cannot read Taskfile vars without extra machinery.
  • Dev dependency rather than a Docker service. Unlike markdownlint and prettier, Igor publishes no image — its goreleaser config has no dockers: section. The composer package is a bootstrapper that downloads the Go binary for the current platform, so vendor/bin/igor-php runs inside the existing phpfpm container. That keeps CLAUDE.md's "never invoke tooling on the host" rule intact with no new image to maintain. The download needs network on first run; it caches under vendor/, which is already gitignored.
  • --no-agent. Igor's richer mode reads a compiled Symfony container. This is a bundle, so there is no application container to read — only the test kernel's, which would be analysing the fixtures rather than the bundle. --no-agent does a static scan instead.
  • Scoped via igor.json, not a CLI path. Unscoped, igor-php . walks vendor/ — 717,028 files, about two minutes, 2,305 "project" findings. With ignore_vendors and the exclude list it is 24 files in under two seconds.
  • Not added to test:matrix. It is a static scan; running it six times across the PHP matrix would tell us the same thing six times.

Note

analyze:worker is now part of task pr:actions, so a local pre-PR run covers it.

Igor reports shared mutable state, which is not the same thing as a leak. The
four values it finds here are shared on purpose — Guzzle clients and their
connection pools, the derived callback paths, the authenticator's logger, and
Symfony's own lazy init of the bundle extension — and igor-baseline.json
records each with a written reason. What the gate protects is the difference:
state that appears without one.

That is the case worth catching. The provider memoization removed in the PKCE
work was exactly this shape, and nothing but review would have stopped it
coming back.

IGOR_VERSION pins the binary the composer bootstrapper fetches, in the Taskfile
and the workflow. Igor is pre-1.0 and its rules move between releases, which
would move the baseline underneath us.
@turegjorup turegjorup self-assigned this Aug 26, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.52%. Comparing base (797cf95) to head (0cf6a0f).

Additional details and impacted files
@@            Coverage Diff             @@
##             develop      #80   +/-   ##
==========================================
  Coverage      99.52%   99.52%           
  Complexity       205      205           
==========================================
  Files             15       15           
  Lines            834      834           
==========================================
  Hits             830      830           
  Misses             4        4           
Flag Coverage Δ
unittests 99.52% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@turegjorup
turegjorup merged commit e7506e9 into develop Aug 26, 2026
18 checks passed
@turegjorup
turegjorup deleted the ci/igor-worker-mode-gate branch August 26, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants