Follow-up to #1120 (PR #1139). That work stopped the report inferring success from silence for the three verification phases: the runner's steps.<id>.outcome is now handed to generate_report.py as PHASE_OUTCOME_<phase> and reconciled against what the phase recorded, so a phase that crashed before writing its verdict is reported as unaccounted instead of rendering as Result: PASSED.
Only three steps carry an id, so only three steps are reconciled. The steps between them are not, and a failure there reproduces the original #1120 symptom by a different route: an issue titled "Langflow Migration Test Failed" whose body opens with Result: PASSED.
What is wrong
migration-test.yml has four steps that can fail after phase_latest succeeded and before phase_nightly_api runs:
| Line |
Step |
How it fails |
migration-test.yml:146 |
Resolve nightly version from PyPI |
explicit exit 1 when PyPI returns no pre-release |
migration-test.yml:167 |
Install Langflow nightly |
uv resolution failure |
migration-test.yml:181 |
Start Langflow nightly (same database) |
process dies immediately |
migration-test.yml:190 |
Wait for Langflow nightly (includes migration) |
180 s timeout — the alembic migration failing to complete is exactly what this workflow exists to catch |
When any of them fails the job stops. Verify migration via API and Verify migration via UI never run, so steps.phase_nightly_api.outcome and steps.phase_nightly_ui.outcome evaluate to the empty string; declared_outcomes() filters empty values, so neither phase is declared. The state file holds only the latest phase, all steps pass. assess() finds no failure, no integrity problem, and returns PASSED.
Generate report runs under if: always(), and Create or update issue on failure runs under if: failure() — so both fire, and the issue body contradicts its own title.
The most valuable failure this workflow can detect — the nightly not booting against a migrated database — lands squarely in this gap.
Reproduction
No CI needed; the script is driven entirely by STATE_FILE + the PHASE_OUTCOME_* variables. This is the exact shape of a run whose nightly never came up:
cat > /tmp/state.json <<'JSON'
{"flow_name":"witness","flow_id":"1",
"phases":{"latest":{"steps":{"create":{"status":"pass"},"execute":{"status":"pass"}}}}}
JSON
env -i PATH="$PATH" STATE_FILE=/tmp/state.json REPORT_FILE=/tmp/report.md \
PHASE_OUTCOME_latest=success PHASE_OUTCOME_nightly_api= PHASE_OUTCOME_nightly_ui= \
python3 tests/github-workflows/migration/generate_report.py | head -8
Observed on main at 56d522b:
# Langflow Migration Test Report
...
## Result: PASSED
For contrast, the same harness on run #115's shape (UI phase declared failure, recorded nothing) correctly yields FAILED + ## Unaccounted phases — that path is fixed and stays fixed.
Options
- Reconcile against
job.status. Pass it in alongside the phase outcomes; a red job whose report has no failure and no integrity entry is itself an integrity problem ("the runner failed this job outside every verified phase — read the log"). One env var, covers all present and future non-phase steps, including ones added later. Preferred.
id + PHASE_OUTCOME_* on the four steps. Precise attribution, but it only covers today's steps and silently degrades the moment someone adds a fifth.
- A declared-phase manifest. The script knows the three phases must exist and flags any that is absent. Catches the gap from the other side, but reports "phase missing" where the real cause is an infra step — accurate but less useful than option 1's message.
Whichever lands, the fix keeps the existing property: nothing may conclude PASSED from missing data.
Done when
Follow-up to #1120 (PR #1139). That work stopped the report inferring success from silence for the three verification phases: the runner's
steps.<id>.outcomeis now handed togenerate_report.pyasPHASE_OUTCOME_<phase>and reconciled against what the phase recorded, so a phase that crashed before writing its verdict is reported as unaccounted instead of rendering asResult: PASSED.Only three steps carry an
id, so only three steps are reconciled. The steps between them are not, and a failure there reproduces the original #1120 symptom by a different route: an issue titled "Langflow Migration Test Failed" whose body opens withResult: PASSED.What is wrong
migration-test.ymlhas four steps that can fail afterphase_latestsucceeded and beforephase_nightly_apiruns:migration-test.yml:146Resolve nightly version from PyPIexit 1when PyPI returns no pre-releasemigration-test.yml:167Install Langflow nightlymigration-test.yml:181Start Langflow nightly (same database)migration-test.yml:190Wait for Langflow nightly (includes migration)When any of them fails the job stops.
Verify migration via APIandVerify migration via UInever run, sosteps.phase_nightly_api.outcomeandsteps.phase_nightly_ui.outcomeevaluate to the empty string;declared_outcomes()filters empty values, so neither phase is declared. The state file holds only thelatestphase, all stepspass.assess()finds no failure, no integrity problem, and returnsPASSED.Generate reportruns underif: always(), andCreate or update issue on failureruns underif: failure()— so both fire, and the issue body contradicts its own title.The most valuable failure this workflow can detect — the nightly not booting against a migrated database — lands squarely in this gap.
Reproduction
No CI needed; the script is driven entirely by
STATE_FILE+ thePHASE_OUTCOME_*variables. This is the exact shape of a run whose nightly never came up:Observed on
mainat 56d522b:For contrast, the same harness on run #115's shape (UI phase declared
failure, recorded nothing) correctly yieldsFAILED+## Unaccounted phases— that path is fixed and stays fixed.Options
job.status. Pass it in alongside the phase outcomes; a red job whose report has no failure and no integrity entry is itself an integrity problem ("the runner failed this job outside every verified phase — read the log"). One env var, covers all present and future non-phase steps, including ones added later. Preferred.id+PHASE_OUTCOME_*on the four steps. Precise attribution, but it only covers today's steps and silently degrades the moment someone adds a fifth.Whichever lands, the fix keeps the existing property: nothing may conclude
PASSEDfrom missing data.Done when
PASSEDtests/github-workflows/migration/test_generate_report.pyreproduces the shape above and asserts the non-PASSEDverdict — same style as the run-fix(playground): review playground-empty-message-send — known bug blocking @stable #115 regression testpython-unitsstays green (uv run --with pytest --no-project python -m pytest tests/github-workflows/migration --ignore=tests/github-workflows/migration/test_ui_migration.py -q)workflow_dispatchrun, or the residual risk stated explicitly in the PR