Investigate test-storybook leaking the Storybook server process
## Summary
The `test-storybook` CI job should fully clean up the Storybook server it starts, leaving no process running after the job script completes. The incident fix stopped the job from failing, but the background server is still leaked; this follow-up completes the teardown so the job is correct rather than only passing.
## Background
During 19.1 monthly release prep, `test-storybook` failed on the GitLab and GitLab FOSS RC tag pipelines with `WaitDelay expired before I/O complete`, blocking the release. The job starts a Storybook server in the background, waits for it, runs `storybook:dev:test`, then runs `kill $STORYBOOK_PID`. That `kill` only reaps the `yarn` wrapper process; the real server child is left orphaned, and because it inherited the runner's trace stream it held that pipe open. Under the legacy runner exec path this leaked descriptor was harmless, but with `FF_SCRIPT_TO_STEP_MIGRATION: true` the runner arms a `WaitDelay`, the orphaned server held the pipe past that window, and the job failed during teardown. The flag did not introduce the bug; it exposed a pre-existing process-cleanup bug that had been hidden before.
The incident fix (!240819, also backported to `19-1`) redirected the background server's output to `/dev/null` so the orphaned child no longer holds the runner's trace stream. That resolved the symptom and unblocked the release, but it addressed only half the root cause: the server process is still orphaned and keeps running after the script, because the cleanup continues to signal only the `yarn` wrapper rather than the actual server. This follow-up completes the cleanup so teardown reaps the whole process tree and no Storybook process survives the job, on both the legacy and step-based runner paths.
We want to keep the following things in mind during implementation:
1. No process started by the job may outlive the script.
2. Teardown must reap the entire server process tree, not just the wrapper process.
3. The job's pass/fail result and cleanup behavior must be independent of the `FF_SCRIPT_TO_STEP_MIGRATION` runner flag.
## Exit Criteria
- [ ] No Storybook server process remains running after the `test-storybook` script completes.
- [ ] `test-storybook` passes with `FF_SCRIPT_TO_STEP_MIGRATION: true`.
- [ ] `test-storybook` continues to pass with the flag unset (no regression on the legacy path).
- [ ] The job no longer relies on output redirection alone to avoid `WaitDelay expired before I/O complete`.
_This ticket was created from_ [_INC-11132_](https://app.incident.io/gitlab/incidents/11132) _using_ [_incident.io_](https://app.incident.io) 🔥
<!-- BEGIN inc-automate enrichment -->
## Contributing cause this Corrective Action addresses
The `test-storybook` CI job starts a Storybook server in the background via `yarn --cwd ./storybook run serve &`, then attempts cleanup with `kill $STORYBOOK_PID`. That signal reaches only the `yarn` wrapper process; the real server child (`http-server`) is spawned as a grandchild and is left orphaned. Because the orphaned child inherited the runner's trace stream descriptor, it held that pipe open after the script completed. With `FF_SCRIPT_TO_STEP_MIGRATION: true`, the runner arms a `WaitDelay` timeout; the lingering descriptor caused the job to fail with `WaitDelay expired before I/O complete` during the 19.1 RC tag pipelines.
The incident fix (MR !240819, backported in MR !240870) redirected the background server's output to `/dev/null`, closing the descriptor leak and unblocking the release. However, the orphaned server process itself continues to run after the script exits. This Corrective Action completes the teardown: cleanup must signal the entire process tree (e.g., via `pkill -P`) rather than only the wrapper, so no Storybook process survives the job on either the legacy or step-based runner path.
## Proposed Action
Implement process-tree teardown in the `test-storybook` job script so that cleanup sends a termination signal to the full child process group rather than only the `yarn` wrapper: replace the `kill $STORYBOOK_PID` call with a whole-tree reap (e.g., `pkill -P $STORYBOOK_PID` or a process-group signal via `kill -- -$STORYBOOK_PID`) (suggested — confirm with assignee), verify that no Storybook server process remains after the script exits, and confirm the job passes with `FF_SCRIPT_TO_STEP_MIGRATION` both set and unset, removing any sole reliance on output redirection to avoid `WaitDelay expired before I/O complete`. Track implementation in [#29239](https://gitlab.com/gitlab-com/gl-infra/production-engineering/-/work_items/29239).
---
_Last updated by [`inc-automate`](https://gitlab.com/gitlab-com/gl-infra/inc-automate) on 2026-06-18 12:15 UTC · synthesis model: `claude-sonnet-4-6` · request id: `6287a0afc22e96cf`._
<!-- END inc-automate enrichment -->
issue
GitLab AI Context
Project: gitlab-com/gl-infra/production-engineering
Instance: https://gitlab.com
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://gitlab.com/gitlab-com/gl-infra/production-engineering/-/raw/main/CONTRIBUTING.md — contribution guidelines
- https://gitlab.com/gitlab-com/gl-infra/production-engineering/-/raw/main/README.md — project overview and setup
Repository: https://gitlab.com/gitlab-com/gl-infra/production-engineering
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD