Skip to content

fix: persist a context switch after its listeners, not before - #51

Merged
refsz merged 1 commit into
mainfrom
fix/context-persists-after-listeners
Aug 25, 2026
Merged

fix: persist a context switch after its listeners, not before#51
refsz merged 1 commit into
mainfrom
fix/context-persists-after-listeners

Conversation

@refsz

@refsz refsz commented Aug 25, 2026

Copy link
Copy Markdown
Owner

From the drupal.z report behind PREFLIGHT.local.md: a context was switched and remembered while the composer install belonging to it never ran. The reachability of the container was the trigger; the order was the cause.

switchTo() persisted, and only then were the listeners dispatched:

state.json   currentContext: staging     written
listener     composer install            failed

Every command after that ran as staging on a project only half prepared for it, and nothing said so. Measured against 0.2.4:

context:switch staging, listener throws

before   [OK] Switched from 'local' to 'staging'    exit 0, staging persisted
after    the work for this context failed           exit 1, nothing persisted
         Current context: local

Why last

Switching a context means preparing the project for it — regenerating templates, reinstalling dependencies — not just writing a name into a file. A switch whose preparation failed should not outlive the process.

What made it safe, and why I checked first

Listeners never needed the persisted value. They read the new context from the event, and SwitchContextOnServices (priority 100) puts the VariableResolver and the TemplateEngine on it before any other listener runs. I verified that before touching the order, because it is the whole reason this is a reordering rather than a redesign.

No API change. switchTo() still validates, switches and persists in one call. The command validates first, dispatches, then switches — so there is no half-applied state, and no persist() that a future caller can forget.

The other half of the report

docs/listeners.md never said how a listener reports failure. It returns nothing, so throwing is the way — a command that fails is not a failure by itself, exactly as in a task. That is now documented with the isSuccessful() check spelled out, because without it the command's output is visible while the run still reports success.

That remains true for events where there is nothing to roll back. Making a failed command inside a listener visible by itself is a separate change — the executor does not know who called it — and I would rather propose it on its own than fold it in here.

Tests

  • E2E: a throwing listener leaves the context where it was, exits non-zero, and context:list still reports the old one
  • E2E: a successful switch is still persisted — the regression guard for the reordering

While writing the first one I had the config wrong (listeners: is not a key — listeners are discovered from tasks.directories, as the docs say and drupal.z does). Worth noting that the top-level schema still accepts an unknown key like that silently; #40 closed variables, not the root.

vendor/bin/phpunit                        786 tests, 1357 assertions, OK (2 new)
vendor/bin/phpstan analyse                [OK] No errors
vendor/bin/php-cs-fixer fix --dry-run     0 of 158 files
vendor/bin/rector --dry-run               [OK]
mkdocs build --strict                     clean

Stacks conceptually with #50 but touches different files, so both can merge in any order.

🤖 Generated with Claude Code

https://claude.ai/code/session_018CTvnzcNYmFgm2HQcm821A

Reported from drupal.z: a context was switched and remembered while the
composer install that belonged to it never ran. The order was the cause -
switchTo() persisted, then the listeners fired:

    state.json   currentContext: staging     written
    listener     composer install            failed

Every command after that ran as staging on a project only half prepared for it,
and nothing said so.

Switching a context means preparing the project for it, so the switch is written
down last. A listener that fails now leaves the previous context active and the
command exits non-zero.

    context:switch staging, listener throws

    before   [OK] Switched from 'local' to 'staging'   exit 0, staging persisted
    after    the work for this context failed          exit 1, nothing persisted
             Current context: local

No API change was needed. switchTo() still validates, switches and persists in
one call - the command validates first, dispatches, and switches afterwards, so
there is no half-applied state and no persist() anyone can forget to call.

Listeners do not need the persisted value: they read the context from the event,
and SwitchContextOnServices (priority 100) puts the resolver and the template
engine on the new one before any other listener runs. Verified before changing
the order, because it is what makes the change safe.

Also documents what was nowhere in docs/listeners.md: a listener returns nothing,
so throwing is how it reports that its work did not happen. Without that check a
failing command is visible in the output while the run still reports success -
which is the other half of this report, and stays true for events where there is
nothing to roll back.
@refsz
refsz force-pushed the fix/context-persists-after-listeners branch from 00557fa to f159faf Compare August 25, 2026 18:28
@refsz
refsz merged commit a1daa6a into main Aug 25, 2026
10 checks passed
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.

1 participant