Skip to content

Commit 64d495f

Browse files
lam0819claude
andauthored
fix!: correct silent-success defects and release v2.0.0 (#58)
* fix: correct silent-success defects and prepare the v0.1.0 release Several paths let the engine do the wrong thing and still report success, which nothing in the suite or a user's logs would have caught. Fixed: - Compound conditions took the wrong branch. ConditionEvaluator had no && / || support but swallowed the operator into the right-hand side and string-compared, so 'order.total > 1000 && order.vip === true' was true for a 500 dollar order. Boolean operators, parentheses and negated groups are now supported; a malformed expression throws instead of collapsing to a boolean. - resume() on a failed workflow threw "Cannot transition from 'failed' to 'failed'" from inside the failure handler, destroying the original cause. FAILED -> RUNNING is now legal, resume retries the failed step, and the handler can no longer mask the real error. - Multi-root workflows executed one root and still reported COMPLETED. - Steps reachable only from each other are rejected at parse time. - Relational comparisons against a missing key are false (null coerces to 0, so 'missing.key < 1000' used to be true). - start() threw instead of overwriting an existing instance. - Workflows blocked on unmet prerequisites park in WAITING. - DelayAction honours the documented minutes/hours keys. - HttpAction reports missing ext-curl as a step failure. Breaking (pre-1.0, each with a migration note in CHANGELOG.md): - EmailAction -> FakeEmailAction; it never sent mail but reported 'status' => 'sent'. Payload is now 'sent' => false, 'mock' => true. - WorkflowBuilder::email() -> fakeEmail(). - ConditionAction drops the non-functional on_true/on_false and uses the shared condition grammar instead of its own '=' / 'is' parser. - FAILED is no longer terminal. Added: - Storage\InMemoryStorage now ships with the package; previously the only adapter lived in tests/ so the library could not run out of the box. - CI runs on pull requests, not just pushes. - SECURITY.md; corrected README action table and attribute examples. - Regression tests for every issue above (116 -> 161 tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: add PHP 8.5 to the test matrix Now installable there since #57 widened the dev tooling constraints. The library accepts php: ^8.3, which admits 8.5, so it should be tested there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 3713b2f commit 64d495f

41 files changed

Lines changed: 1878 additions & 289 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/phpstan.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ on:
77
- 'phpstan.neon.dist'
88
- '.github/workflows/phpstan.yml'
99

10+
pull_request:
11+
paths:
12+
- '**.php'
13+
- 'phpstan.neon.dist'
14+
- '.github/workflows/phpstan.yml'
15+
1016
jobs:
1117
phpstan:
1218
name: phpstan

‎.github/workflows/run-tests.yml‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ on:
99
- 'composer.json'
1010
- 'composer.lock'
1111

12-
12+
13+
pull_request:
14+
paths:
15+
- '**.php'
16+
- '.github/workflows/run-tests.yml'
17+
- 'phpunit.xml.dist'
18+
- 'composer.json'
19+
- 'composer.lock'
20+
21+
1322
jobs:
1423
test:
1524
runs-on: ${{ matrix.os }}
@@ -18,7 +27,7 @@ jobs:
1827
fail-fast: true
1928
matrix:
2029
os: [ubuntu-latest]
21-
php: [8.4, 8.3]
30+
php: [8.5, 8.4, 8.3]
2231
stability: [prefer-lowest, prefer-stable]
2332

2433
name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}

‎CHANGELOG.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,108 @@ All notable changes to `workflow-engine-core` will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## v2.0.0 - 2026-09-12
9+
10+
Fixes several cases where the engine silently did the wrong thing and still
11+
reported success — the failure mode a workflow engine can least afford.
12+
13+
**Why 2.0.0 and not 1.1.0:** `v1.0.0` was tagged as a stable release, so the
14+
corrections below — which change public class names and runtime behaviour — are
15+
breaking changes under semver and require a major bump. Every one is listed with
16+
its migration. If you are on `v0.0.4-alpha` or `v1.0.0`, read the *Changed*
17+
section before upgrading.
18+
19+
### Fixed
20+
21+
- **Compound conditions no longer evaluate to the wrong branch.** `ConditionEvaluator`
22+
had no `&&`/`||` support, but instead of rejecting them it swallowed the operator
23+
into the right-hand side and fell back to a string comparison — so
24+
`order.total > 1000 && order.vip === true` returned **true** for a 500 dollar
25+
order, with no exception and nothing in the logs. Boolean operators, parentheses
26+
and negated groups are now supported, and a malformed expression throws
27+
`InvalidWorkflowDefinitionException` instead of collapsing to a boolean.
28+
- **`resume()` works on a failed workflow.** Recovering a failed instance — the
29+
documented recovery path — threw `Cannot transition workflow from 'failed' to
30+
'failed'`. Three defects chained: the executor never lifted `FAILED` back to
31+
`RUNNING`; the final `FAILED -> COMPLETED` hop was rejected; and the resulting
32+
exception was raised *inside* the failure handler, destroying the original cause.
33+
`FAILED -> RUNNING` is now a legal transition, resume retries the step that
34+
failed, and the failure handler can no longer mask the real error.
35+
- **Multi-root workflows no longer drop a branch.** A definition with two
36+
independent entry points executed only one of them and still reported
37+
`COMPLETED` (with progress stuck below 100%). Every root now runs.
38+
- **Unreachable steps are rejected at parse time.** A group of steps reachable
39+
only from each other could never execute, yet the workflow reported success.
40+
- **Relational comparisons against a missing key are `false`.** `null` coerces to
41+
`0` in PHP, so `missing.key < 1000` was *true* and steps gated on data that was
42+
never set would run.
43+
- **`start()` no longer overwrites an existing instance.** Reusing a workflow ID
44+
silently discarded the earlier run's state and history; it now throws
45+
`InvalidWorkflowStateException`.
46+
- **Blocked workflows park in `WAITING`.** A workflow whose steps were all blocked
47+
on unmet prerequisites stayed in `RUNNING` forever, indistinguishable from one
48+
still executing.
49+
- **`DelayAction` honours `minutes` and `hours`.** Both were documented but never
50+
read, so `delay(hours: 2)` silently paused for the one second default.
51+
- **`HttpAction` reports a missing cURL extension** as a step failure instead of a
52+
fatal "undefined function" error.
53+
54+
### Changed
55+
56+
- **BREAKING: `EmailAction` is now `FakeEmailAction`.** It never sent email, but
57+
returned `'status' => 'sent'` — a workflow could show a delivered confirmation
58+
that did not exist. The payload is now explicitly `'sent' => false, 'mock' => true`,
59+
and it logs a warning. *Migration: implement `WorkflowAction` with your own mail
60+
transport for real delivery.*
61+
- **BREAKING: `WorkflowBuilder::email()` is now `fakeEmail()`**, for the same
62+
reason. *Migration: rename the call, or switch to your own action.*
63+
- **BREAKING: `ConditionAction` no longer accepts `on_true` / `on_false`.** They
64+
were read but never consumed by the engine, so the documented branching did not
65+
exist. *Migration: branch with a `condition` on the transition instead.*
66+
- **BREAKING: `ConditionAction` uses the shared condition grammar.** It previously
67+
carried its own parser accepting `=`, `is` and `is not`, which no other part of
68+
the engine understood. *Migration: use `===`, `==`, `!=` etc.*
69+
- **BREAKING: `FAILED` is no longer a terminal state** — it can transition to
70+
`RUNNING` (resume) or `CANCELLED`. Code asserting that failed workflows are
71+
immutable needs updating.
72+
- `WorkflowState::isFinished()` still reports `true` for `FAILED`; use
73+
`canTransitionTo()` to test whether an instance can still move.
74+
75+
### Added
76+
77+
- `Storage\InMemoryStorage` now **ships with the package**. Previously the only
78+
implementation lived in `tests/` under `autoload-dev`, so the library could not
79+
run a workflow out of the box without first writing an adapter.
80+
- `WorkflowDefinition::getFirstSteps()` returns every entry point.
81+
- `Support\Arr::has()` distinguishes an absent key from one holding `null`.
82+
- Boolean operators (`&&`, `||`), parentheses and negated groups in conditions.
83+
- `SECURITY.md` with a disclosure process and scope notes.
84+
- Regression tests for every issue above (116 -> 161 tests).
85+
86+
### CI
87+
88+
- **PHP 8.5 added to the test matrix.** The library accepts `php: ^8.3`, which
89+
admits 8.5, but it was never tested there. (Installing on 8.5 became possible
90+
once #57 widened the dev tooling constraints.)
91+
- `run-tests.yml` and `phpstan.yml` now run on **pull requests**, not just pushes.
92+
Combined with `dependabot-auto-merge.yml` auto-merging minor and patch bumps,
93+
dependency updates could previously reach `main` without the suite ever running
94+
against the merge result.
95+
- Removed a stale PHPStan `ignoreErrors` pattern that no longer matched anything.
96+
97+
### Docs
98+
99+
- Documented the condition grammar, including the two rules that stop a mistyped
100+
condition from silently routing a workflow the wrong way.
101+
- Corrected the built-in actions table: it documented a `body` key for
102+
`EmailAction` and `HttpAction` that neither read, `minutes`/`hours` for
103+
`DelayAction` that were ignored, and branching for `ConditionAction` that did
104+
not exist.
105+
- Fixed the PHP attribute examples, whose inline comments claimed retries and
106+
timeouts that the attributes do not actually perform — the engine still does not
107+
read them (this is noted in the README).
108+
- Archived the completed `PLAN.md` to `docs/PLAN-2025-refactor.md`.
109+
8110
## v1.0.0 - 2026-09-12
9111

10112
First stable release.

‎CLAUDE.md‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Project context for Claude Code and AI-assisted development.
66

77
**workflow-engine-core** is a framework-agnostic PHP workflow engine. Zero production dependencies. PHP 8.3+. MIT licensed.
88

9-
Status: **v0.0.2-alpha** — active development, not production-ready.
9+
Status: **v2.0.0** — stable. Breaking changes follow semver and are documented in CHANGELOG.md.
1010

1111
Related package: `solution-forest/workflow-engine-laravel` (Laravel integration layer).
1212

@@ -50,31 +50,36 @@ WorkflowBuilder → WorkflowDefinition → WorkflowEngine → Executor → Actio
5050
| Namespace | Purpose |
5151
|-----------|---------|
5252
| `Core\` | WorkflowEngine, WorkflowBuilder, Executor, StateManager, WorkflowInstance, WorkflowDefinition, WorkflowContext, ActionResult, Step, DefinitionParser, ActionResolver |
53-
| `Actions\` | BaseAction, LogAction, EmailAction, HttpAction, DelayAction, ConditionAction |
53+
| `Actions\` | BaseAction, LogAction, FakeEmailAction, HttpAction, DelayAction, ConditionAction |
5454
| `Contracts\` | WorkflowAction, StorageAdapter, EventDispatcher, Logger |
5555
| `Attributes\` | WorkflowStep, Retry, Timeout, Condition |
5656
| `Events\` | WorkflowStartedEvent, WorkflowCompletedEvent, WorkflowFailedEvent, WorkflowCancelledEvent, StepCompletedEvent, StepFailedEvent, StepRetriedEvent |
5757
| `Exceptions\` | WorkflowException (base), InvalidWorkflowDefinitionException, InvalidWorkflowStateException, ActionNotFoundException, StepExecutionException, WorkflowInstanceNotFoundException |
5858
| `Support\` | NullLogger, NullEventDispatcher, SimpleWorkflow, Uuid, Timeout, ConditionEvaluator, Arr |
59+
| `Storage\` | InMemoryStorage (ships with the package; non-durable) |
5960

6061
### State Machine
6162

6263
```
63-
PENDING → RUNNING → COMPLETED
64-
↓ ↓ ↑
65-
FAILED WAITING
66-
↑ ↓ ↑
67-
FAILED ← PAUSED
68-
↑
69-
CANCELLED ← (any non-terminal state)
64+
PENDING ──→ RUNNING ──→ COMPLETED (terminal)
65+
↓ ↑
66+
WAITING / PAUSED
67+
↓ ↑
68+
FAILED ──→ RUNNING (resume retries the failed step)
69+
↓
70+
CANCELLED (terminal)
7071
```
7172

7273
**Valid transitions (enforced at runtime):**
7374
- `PENDING` → `RUNNING`, `FAILED`, `CANCELLED`
7475
- `RUNNING` → `WAITING`, `PAUSED`, `COMPLETED`, `FAILED`, `CANCELLED`
7576
- `WAITING` → `RUNNING`, `FAILED`, `CANCELLED`
7677
- `PAUSED` → `RUNNING`, `FAILED`, `CANCELLED`
77-
- Terminal states (`COMPLETED`, `FAILED`, `CANCELLED`) → no transitions allowed
78+
- `FAILED` → `RUNNING` (recovery via `resume()`), `CANCELLED`
79+
- Terminal states (`COMPLETED`, `CANCELLED`) → no transitions allowed
80+
81+
`FAILED` is recoverable: `resume()` returns the instance to `RUNNING` and retries
82+
the step that failed. A workflow blocked on unmet prerequisites parks in `WAITING`.
7883

7984
Invalid transitions throw `InvalidWorkflowStateException`.
8085

@@ -154,15 +159,15 @@ $engine->cancel($instanceId, 'reason');
154159
## CI/CD
155160

156161
GitHub Actions workflows:
157-
- `run-tests.yml` — Matrix: PHP 8.3/8.4 × prefer-lowest/prefer-stable
158-
- `phpstan.yml` — Static analysis on .php changes
162+
- `run-tests.yml` — Matrix: PHP 8.3/8.4 × prefer-lowest/prefer-stable (runs on push **and** pull_request)
163+
- `phpstan.yml` — Static analysis on .php changes (runs on push **and** pull_request)
159164
- `fix-php-code-style-issues.yml` — Auto-format with Pint on push
160165
- `update-changelog.yml` — Auto-update CHANGELOG on release
161166
- `dependabot-auto-merge.yml` — Auto-merge minor/patch dependency updates
162167

163168
## File Counts
164169

165-
- 46 source files in `src/`
166-
- 25 test files in `tests/`
167-
- 93 tests, 224+ assertions
170+
- 47 source files in `src/`
171+
- 27 test files in `tests/`
172+
- 161 tests, 363+ assertions
168173
- PHPStan level 6 compliance

0 commit comments

Comments
 (0)