Skip to content

feat(plugin-dev): add watch mode, readiness polling, and phased start UX - #222

Merged
gashcrumb merged 11 commits into
redhat-developer:mainfrom
gashcrumb:feat/plugin-dev-watch
Sep 28, 2026
Merged

gashcrumb merged 11 commits into
redhat-developer:mainfrom
gashcrumb:feat/plugin-dev-watch

Conversation

@gashcrumb

Copy link
Copy Markdown
Member

Summary

Adds continuous watch mode to plugin dev, readiness polling, and a phased progress UX for start (RHIDP-16673).

plugin dev start --watch / plugin dev update --watch

  • New --watch flag on start: after the runtime is up and ready, falls straight into watch mode instead of exiting
  • New rhdh-cli plugin dev update --watch: watches src/ and package.json (chokidar, 500ms debounce) and re-runs the same export/stage/restart cycle as a one-shot update on every change
  • Cycles are serialized — a change arriving while a cycle is running queues exactly one follow-up rather than overlapping
  • Waits for both the RHDH and installer containers to emit their terminal event (cleanup on Podman, die on Docker) before starting the next cycle, avoiding a crun exec.fifo race on back-to-back restarts
  • Prints Refresh your browser at <url> once RHDH responds to a readiness poll

plugin dev start — phased UX

  • Shows four labeled phases ([1/4]–[4/4]): build/export, start runtime, install plugins, wait for readiness
  • [3/4] now watches the installer container's died event instead of blocking on a Compose call, and skips straight through on a re-entrant start if the installer already exited in a prior run (previously stalled for the full 60s event-wait timeout)
  • [4/4] polls HTTP until RHDH responds, then prints the URL

plugin dev update / restart — runtime pre-flight

  • Both now fail fast with RHDH Local is not running. Run \rhdh-cli plugin dev start` first.` instead of a raw Compose/container error, when the runtime isn't up yet

Also fixed

  • isIgnoredWatchPath: chokidar v4+ dropped glob-string support for the ignored option (function/regex/literal path only) — the previous glob array silently matched nothing. Replaced with a path-segment predicate that actually excludes node_modules/, dist/, dist-dynamic/, and dist-types/ from the watcher.

Testing

  • Unit tests: waitForContainerEvent/waitForContainerCleanup, resolveRhdhUrl, waitForRhdhReady, watchUpdate (cycle trigger, debounce, recovery, SIGINT, not-running guard), isIgnoredWatchPath, start --watch entry/skip, update/restart pre-flight guards
  • Manually verified against a real RHDH Local instance

Closes #RHIDP-16673

… UX (RHIDP-16673)

plugin dev update --watch
----------------------
- Add chokidar file watcher on src/ and package.json with 500ms debounce
- Extract runUpdateCycle() so one-shot and watch share the same implementation
- Serialize cycles: a change arriving during an active cycle queues exactly
  one follow-up rather than running concurrently
- Add waitForContainerEvent(tool, service, action, timeoutMs): subscribe to
  container runtime event stream via `podman|docker events --stream`, resolve
  when the target service emits the target action. Shared primitive used by
  both cleanup settling and start phase progress.
- Add waitForContainerCleanup(tool, timeoutMs): waits for both rhdh and
  install-dynamic-plugins to emit their terminal event before the next
  back-to-back cycle starts, preventing the crun exec.fifo race:
    Podman: cleanup (after crun tears down exec.fifo)
    Docker: die    (terminal event; no equivalent cleanup step)
- Print '[watch] Refresh your browser at <url>' when RHDH is ready
- Fix isIgnoredWatchPath: chokidar v4+ dropped glob-string support for
  `ignored` (function/regex/literal path only). The previous glob array
  (e.g. double-star dist/node_modules patterns) silently matched nothing,
  verified by reproduction against the vendored chokidar. Replaced with a
  path-segment predicate that actually excludes output directories.

plugin dev start — phased UX and --watch
-----------------------------------------
- Show four labeled phases: build/export, start runtime, install plugins,
  wait for readiness
- [3/4] Installing dynamic plugins uses waitForContainerEvent to watch for
  the installer container's died event rather than blocking on a compose call
- waitForInstallerToFinish checks the installer's current Compose state
  before subscribing to the events stream: compose up -d won't recreate an
  already-exited one-shot container, so a re-entrant `start` against an
  already-running runtime previously stalled for the full 60s
  waitForContainerEvent timeout waiting on a `died` event that already fired
  in a prior invocation. Skips straight through when already exited.
- [4/4] polls HTTP until RHDH responds, then prints the URL
- Add --watch to `start`: after readiness, fall straight into watchUpdate()
  so a user can go from a cold start into continuous watch mode in one
  command. --configure stays independent — no implicit behavior change.

plugin dev update / restart — runtime pre-flight checks
---------------------------------------------------------
- Add ensureRuntimeRunning(tool, runtimeDir): inspects Compose ps output for
  the rhdh service and fails fast with an actionable message
  ("RHDH Local is not running. Run `rhdh-cli plugin dev start` first.")
  instead of a raw compose/container subprocess error. Applied to both
  runUpdateCycle (covers update and update --watch) and restart, the only
  two subcommands with a real "runtime must already exist" precondition —
  confirmed via a full precondition/action/postcondition pass over all six
  plugin dev subcommands (start, update, restart, stop, logs, status).
- Refactored formatRuntimeStatus's inline service-lookup closures into
  shared findComposeService/composeServiceState/composeServiceExitCode
  helpers, reused by ensureRuntimeRunning, waitForInstallerToFinish, and
  formatRuntimeStatus itself.
- Fixed ensureGeneratedConfigIncluded's error message: it told users to
  "rerun with --configure", but that flag only exists on `start`, not on
  `update` — a dead end for a user hitting this on `update --watch`. Now
  points at `rhdh-cli plugin dev start --configure` directly.

Shared helpers
--------------
- resolveRhdhUrl(runtimeDir): reads BASE_URL from default.env then .env
  (optional override), falls back to http://localhost:7007
- waitForRhdhReady(url, timeoutMs, pollIntervalMs): fetch-polls with dots,
  resolves with the URL, throws with an actionable message on timeout

Testing
-------
- waitForContainerEvent: resolves on match, ignores wrong service, times out,
  passes correct filter to spawn
- waitForContainerCleanup: podman uses cleanup, docker uses die
- resolveRhdhUrl: fallback, default.env, .env override, commented lines
- waitForRhdhReady: resolves on 200, retries on ECONNREFUSED, throws on timeout
- watchUpdate: cycle fires, debounce coalesces, recovery after failure,
  SIGINT, fails cleanly and keeps watching when RHDH Local is not running
- isIgnoredWatchPath: excludes output/dependency directories, does not
  exclude ordinary watched paths (regression test for the chokidar v4 glob
  behavior change — exercises the real predicate, not a mocked watcher)
- start: enters watch mode on --watch, does not enter it otherwise, skips
  the installer event wait when it already exited (re-entrant start)
- update / restart: reject with an actionable message when RHDH Local is
  not running or stopped; restart succeeds and issues both compose calls
  when running

Signed-off-by: Stan Lewis <stlewis@redhat.com>
Assisted-by: claude-sonnet-4-6@default

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
Assisted-By: opencode

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
- Simplify the BASE_URL regex in resolveRhdhUrl to remove a superlinear
  backtracking pattern (typescript:S8786): capture the rest of the line
  greedily and trim() afterward instead of combining a lazy capture with
  a trailing \s*$ anchor.
- Add describeWatchError(err) to stringify caught unknown values in
  watchUpdate's cycle-failure and watcher-error handlers, instead of
  String(err), which falls back to Object.prototype.toString()
  ('[object Object]') for plain objects (typescript:S6551 x2).

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
Assisted-By: opencode

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
The prior fix still combined two adjacent quantifiers over overlapping
character classes (\s* then .*), which SonarCloud continued to flag as
superlinear (typescript:S8786). Replace the whole BASE_URL match with a
plain indexOf('=')/slice() split — no regex needed for the key/value
parse, only the existing anchored quote-strip regex remains.

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
Assisted-By: opencode

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
The watchUpdate and start suites each carried a near-identical block that
mkdtemp'd a runtime/plugin dir, wrote the RHDH Local marker files, and wrote
a minimal frontend plugin package.json. Extract scaffoldPluginDevRuntime()
and have both beforeEach hooks call it. Also collapse the repeated
fs.writeFile(path.join(dir, name), content) calls in the resolveRhdhUrl
suite into a small writeEnvFile() closure. Pulled new_duplicated_lines_density
back under the 3% quality-gate threshold (was 7.7%, all in this file); no
behavior change, same 49 tests still pass.

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
Assisted-By: opencode

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@gashcrumb

Copy link
Copy Markdown
Member Author

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:56 AM UTC · Completed 12:17 PM UTC

Commit: a06a9fc · View workflow run →

Runtime: claude · Model: sonnet → claude-sonnet-4-6 · Effort: high · Cost: $5.84

@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Sep 25, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Risk Assessment: moderate (2/5)

Details

Moderate risk driven by large change size (1345 lines, large blast radius) and two dependency files changed, partially offset by no security-sensitive paths, no CI workflow changes, a healthy test-file ratio, and no churn or revert history on the newly-introduced dev command files.

Previous run

Risk Assessment: moderate (2/5)

Details

Moderate risk driven by large change size (1297 lines, large blast radius) and two dependency files changed, partially offset by no security-sensitive paths, no CI changes, and low churn on the newly-introduced dev command files.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review

Findings

Medium

  • [api-contract-violation] src/commands/dev/command.ts:329 — waitForContainerEvent passes '--stream' in the spawn args unconditionally. This flag is not supported by Docker's events command (Docker streams by default); Docker rejects it, exits immediately, and child.on('close') resolves the promise before any event is received. On Docker, phase 3 of plugin dev start (waitForInstallerToFinish) becomes a silent no-op — the installer is never waited for. Phase 4's HTTP readiness poll partly compensates, but the phase-3 guarantee is lost on Docker.
    Remediation: Remove '--stream' from the spawn args entirely (both runtimes stream by default), or conditionally include it: ...(containerTool === 'podman' ? ['--stream'] : []).

  • [protected-path] AGENTS.md — This PR modifies AGENTS.md, which is a protected governance path. Human approval is required for protected-path changes regardless of context. The PR links to RHIDP-16673 and the description explains the rationale (documenting new plugin dev behaviors).

Low

  • [api-shape] src/commands/dev/command.ts:276 — runUpdateCycle(runtimeDir, containerTool, prefix) and watchUpdate(runtimeDir, containerTool, ...) lead with runtimeDir. All other multi-param helpers in the file (getComposeServices, getRuntimeStatus, ensureRuntimeRunning, compose, waitForInstallerToFinish, waitForContainerCleanup) lead with containerTool, then runtimeDir. The reversed order is an inconsistency that could produce silent argument-swap bugs at call sites.
    Remediation: Reorder to (containerTool, runtimeDir, ...) to match the file-wide convention. Update all call sites.

  • [edge-case] src/commands/dev/command.ts:299 — update --watch enters watchUpdate without calling ensureRuntimeRunning first. The runtime check only fires on the first change-triggered cycle. A user running plugin dev update --watch against a stopped runtime sees no error until they edit a source file — inconsistent with the one-shot update path, which fails fast immediately.
    Remediation: Call ensureRuntimeRunning(runtimeDir, containerTool) at the top of the update handler when opts.watch is true, before entering watchUpdate.

  • [logic-error] src/commands/dev/command.ts:74 — The readiness check res.ok || (res.status >= 200 && res.status < 400) is a tautology: res.ok covers [200, 300), which is already a subset of [200, 400). The res.ok branch is dead code. The condition works correctly, but the intent (accepting 3xx redirects as ready) is not obvious from the compound form.
    Remediation: Simplify to res.status >= 200 && res.status < 400, or add a comment explaining that 3xx responses are intentionally treated as ready.

  • [code-organization] src/commands/dev/command.ts:561 — watchUpdate registers two separate watcher.on('error', ...) listeners: one that logs via Task.log and one that rejects the keep-alive Promise. Both fire on every watcher error. Behavior is correct (Node.js calls listeners in registration order) but two handlers on the same event obscures the control flow intent compared to the rest of the file.
    Remediation: Consolidate into a single handler: watcher.on('error', err => { Task.log('[watch] Watcher error: ...'); reject(err); });


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

Medium

  • [logic-error] src/commands/dev/command.ts:239 — waitForInstallerToFinish hardcodes the event action 'died' (Podman-specific) for container death. Docker uses 'die', not 'died'. On Docker, the --filter event=died argument to docker events never matches any event, causing phase 3 of plugin dev start to stall for the full 60-second timeout on every Docker-backed run. waitForContainerCleanup already handles this difference correctly (containerTool === 'podman' ? 'cleanup' : 'die'), making the omission in waitForInstallerToFinish a clear oversight.
    Remediation: const diedEvent = containerTool === 'podman' ? 'died' : 'die'; await waitForContainerEvent(containerTool, 'install-dynamic-plugins', diedEvent);

  • [api-shape] src/commands/dev/index.ts:20 — The barrel file's established convention (AGENTS.md) is to re-export only the six CLI action handlers (start, update, restart, stop, logs, status). This PR adds five internal helpers (watchUpdate, waitForContainerEvent, waitForContainerCleanup, resolveRhdhUrl, waitForRhdhReady) that have no external consumer — tests import them directly from ./command, and src/commands/index.ts only accesses m.start/m.update via lazy import. Re-exporting them through the barrel widens the module's apparent public surface without a driving consumer.
    Remediation: Remove watchUpdate, waitForContainerEvent, waitForContainerCleanup, resolveRhdhUrl, and waitForRhdhReady from src/commands/dev/index.ts, keeping only the six command handler re-exports.

  • [missing-doc] README.md:88 — The "Testing a Plugin in RHDH Local" section describes plugin dev start and plugin dev update but does not mention the new --watch flag, which enables continuous re-export/re-stage/restart on source changes (available on both subcommands). The flag is discoverable via --help and CHANGELOG, but the README is the primary entry point for new users.
    Remediation: Add a --watch usage example or description to the plugin dev start/plugin dev update section.

  • [stale-doc] README.md:88 — plugin dev start now runs four labeled phases ([1/4]–[4/4]) and blocks until RHDH is reachable, printing the URL. The README describes it only as "exports it and runs it against an existing RHDH Local checkout", setting incorrect expectations about duration and output.
    Remediation: Update the plugin dev start prose to describe the phased execution and URL output on readiness.

  • [stale-doc] README.md:92 — plugin dev update now blocks for up to ~120 s waiting for RHDH to become ready and prints "Refresh your browser at <url>". The README describes it as "refresh the staged plugin and RHDH service" with no indication it now blocks.
    Remediation: Update the plugin dev update description to note the readiness polling and URL output.

Low

  • [doc-style] src/commands/dev/command.ts:459 — The JSDoc for watchUpdate states "Watches src/, package.json, tsconfig*.json, and .config. files" but watchPaths contains only src/ and package.json. The runtime log message correctly lists only the actual paths; the JSDoc reflects an earlier design iteration.
    Remediation: Remove "tsconfig*.json, and .config. files" from the JSDoc bullet.

  • [ssrf] src/commands/dev/command.ts:73 — waitForRhdhReady polls a URL read from user-controlled env files (default.env, .env) without scheme or hostname validation. Threat model is bounded to self-inflicted misconfiguration in this local developer CLI context.

  • [missing-doc] README.md:97 — plugin dev update and plugin dev restart now require RHDH Local to already be running and fail fast with a clear message if it is not. This precondition is not documented in the README.
    Remediation: Note in the plugin dev update/plugin dev restart descriptions that both require RHDH Local to be running.

  • [stale-doc] AGENTS.md:120 — The plugin dev section does not mention --watch on start/update, the readiness polling behavior, or the new ensureRuntimeRunning pre-flight check that guards update and restart.
    Remediation: Update the plugin dev section in AGENTS.md to reflect the new behaviors.

  • [authorization-non-github] N/A — Authorization is via Jira ticket RHIDP-16673 (not accessible via GitHub API). The PR body provides sufficient detail; the author is an identified team member. Teams using GitHub-only authorization workflows should confirm the ticket covers all three features (watch mode, readiness polling, phased UX) and the incidental fixes.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

- Fix waitForInstallerToFinish hardcoding Podman's 'died' event action
  unconditionally: on Docker (--container-tool docker), the terminal
  container-death event is 'die', not 'died', so the --filter event=died
  argument to 'docker events' never matched anything and phase 3 of
  plugin dev start stalled for the full 60s waitForContainerEvent
  timeout on every Docker-backed run. Branch on containerTool the same
  way waitForContainerCleanup already does. Add a regression test that
  asserts the docker branch subscribes with event=die, not event=died.
- Trim src/commands/dev/index.ts back down to the six CLI action
  handlers (start, update, restart, stop, logs, status) per this
  repo's barrel-file convention (AGENTS.md). watchUpdate,
  waitForContainerEvent, waitForContainerCleanup, resolveRhdhUrl, and
  waitForRhdhReady have no consumer outside command.ts/command.test.ts
  (tests already import them directly from ./command) and were
  widening the module's public surface without a driving need.
- Fix stale watchUpdate JSDoc claiming it watches tsconfig*.json and
  *.config.* files; watchPaths only ever contained src/ and
  package.json.
- README: document --watch on start/update, start's phased [1/4]-[4/4]
  output and blocking-until-ready behavior, update's readiness-poll
  blocking (up to 2 minutes) and URL output, and that update/restart
  require the runtime to already be running.
- AGENTS.md: document --watch, the readiness-poll blocking behavior on
  start/update, and the ensureRuntimeRunning pre-flight check on
  update/restart.

Addresses fullsend-ai-review findings on PR redhat-developer#222 (inline comments on
command.ts:239, dev/index.ts:20, command.ts:459, and the PR-level
summary comment covering README.md/AGENTS.md staleness). The
'authorization-non-github' note and the 'ssrf' note on
waitForRhdhReady (informational only, no remediation suggested; the
poll target is local developer configuration, not an external input)
require no code change.

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
Assisted-By: opencode

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@gashcrumb

Copy link
Copy Markdown
Member Author

Addressed in dbc1748:

  • logic-error (command.ts:239): fixed — waitForInstallerToFinish now branches 'died'/'die' on containerTool, same as waitForContainerCleanup. Added a regression test for the Docker branch.
  • api-shape (dev/index.ts:20): fixed — barrel trimmed back to the six action handlers.
  • doc-style (command.ts:459): fixed — stale tsconfig*.json/*.config.* JSDoc clause removed.
  • missing-doc / stale-doc (README.md:88, 92, 97): fixed — documented --watch on start/update, start's phased [1/4]\u2013[4/4] output and blocking-until-ready behavior, update's readiness-poll blocking (up to 2 min) and URL output, and that update/restart require the runtime to already be running.
  • stale-doc (AGENTS.md:120): fixed — documented --watch, the readiness-poll blocking behavior, and the ensureRuntimeRunning pre-flight check.
  • ssrf (command.ts:73): no code change — replied inline with the threat-model reasoning (trusted local checkout, same boundary the rest of plugin dev already operates inside).
  • authorization-non-github: informational only, no action needed — RHIDP-16673 covers all three features plus the incidental fixes.

All 50 unit tests, lint, prettier, and tsc pass; SonarCloud quality gate is green.

@gashcrumb

Copy link
Copy Markdown
Member Author

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:55 PM UTC · Completed 1:15 PM UTC

Commit: dbc1748 · View workflow run →

Runtime: claude · Model: sonnet → claude-sonnet-4-6 · Effort: high · Cost: $5.30

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread src/commands/dev/command.ts Outdated
Comment thread src/commands/dev/command.ts
Comment thread src/commands/dev/command.ts
Comment thread src/commands/dev/command.ts Outdated
Comment thread src/commands/dev/command.ts Outdated
- Fix api-contract-violation: waitForContainerEvent passed '--stream'
  unconditionally, but Docker's 'events' command has no such flag (it
  always streams) and rejects unknown flags, exiting immediately.
  child.on('close') then resolved the promise before any event was
  seen, making phase 3 of 'plugin dev start' (waitForInstallerToFinish)
  a silent no-op on Docker. Only pass --stream for Podman. Added a
  regression test asserting docker args omit --stream and podman args
  include it; confirmed it fails against the unfixed code.
- Fix edge-case: 'update --watch' entered watchUpdate without calling
  ensureRuntimeRunning first, so a stopped runtime only surfaced an
  error on the first change-triggered cycle instead of failing fast
  like the one-shot 'update' path. Call ensureRuntimeRunning before
  entering watch mode. Added a regression test.
- Fix logic-error: the readiness check
  'res.ok || (res.status >= 200 && res.status < 400)' was a tautology
  (res.ok's [200,300) is already covered by [200,400)), obscuring the
  intent to also accept 3xx redirects. Simplified to the range check
  alone with a clarifying comment.
- Fix api-shape: runUpdateCycle and watchUpdate led with runtimeDir,
  while every other multi-param helper in the file
  (getComposeServices, ensureRuntimeRunning, compose,
  waitForInstallerToFinish, waitForContainerCleanup) leads with
  containerTool. Reordered both to (containerTool, runtimeDir, ...)
  and updated all call sites (command.ts and command.test.ts).
- Fix code-organization: watchUpdate registered two separate
  watcher.on('error', ...) listeners (one logging, one rejecting).
  Consolidated into a single handler that does both.

Addresses fullsend-ai-review findings on PR redhat-developer#222 at commit dbc1748.
The 'protected-path' note on AGENTS.md is acknowledged — that edit
was made in the prior commit at the PR author's explicit direction
to document the RHIDP-16673 behaviors landing in this same PR.

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
Assisted-By: opencode

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@gashcrumb

Copy link
Copy Markdown
Member Author

Second review round addressed in f4ae23e:

  • api-contract-violation (command.ts:329, medium): fixed — --stream is now Podman-only; Docker's events has no such flag and was silently exiting before ever seeing an event. Added a regression test.
  • api-shape (command.ts:276, low): fixed — runUpdateCycle/watchUpdate reordered to (containerTool, runtimeDir, ...) to match every other helper in the file; all call sites updated.
  • edge-case (command.ts:299, low): fixed — update --watch now calls ensureRuntimeRunning up front instead of only discovering a stopped runtime on the first change-triggered cycle. Added a regression test.
  • logic-error (command.ts:74, low): fixed — removed the tautological res.ok || branch, kept the range check with a comment on the 3xx intent.
  • code-organization (command.ts:561, low): fixed — consolidated the two watcher.on('error', ...) listeners into one.
  • protected-path (AGENTS.md): acknowledged, no further action — that edit was made in the prior commit (dbc1748) at my explicit direction, to document the RHIDP-16673 plugin dev behaviors landing in this same PR.

All 52 unit tests, lint, prettier, and tsc pass; SonarCloud quality gate is green with zero open issues.

@PatAKnight PatAKnight left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of comments and some nits

Comment thread src/commands/dev/command.ts Outdated
Comment thread src/commands/dev/command.ts Outdated
Comment thread src/commands/dev/command.ts
Comment thread src/commands/dev/command.ts Outdated
Comment thread src/commands/dev/command.ts
Comment thread src/commands/dev/command.ts Outdated
Comment thread src/commands/dev/command.ts
Seven findings from PatAKnight's review round:

- Hold running through the settle-wait instead of clearing it in a
  finally block right after the cycle. A change arriving during the
  wait previously saw running === false and started a second,
  concurrent drainCycles() loop instead of being coalesced into the
  active one. Also move the settle-wait's event subscription to the
  top of each cycle (before runUpdateCycle's own compose stop/start
  calls run), since subscribing afterward almost always misses the
  event and burns the full settleTimeoutMs.
- Fix Docker's events JSON shape: the compose-service label lives
  under Actor.Attributes for Docker, not the top-level Attributes
  field Podman uses (verified against real podman events --format
  json output and Docker's documented schema). Reading only the
  top-level field meant the filter never matched on Docker, and the
  events-shape bug was masked by a prior regression test that emitted
  Podman's shape for a Docker scenario -- fixed that test too.
- Watch tsconfig.json in addition to src/ and package.json --
  RHIDP-16673's acceptance criteria require watching "relevant build
  configuration files"; tsconfig.json is the only one plugin new
  scaffolds.
- Scope isIgnoredWatchPath's segment matching to the plugin root
  (chokidar always passes absolute paths) instead of checking every
  segment of the full absolute path, which ignored every file when a
  checkout merely lived under an ancestor directory named e.g. dist
  or node_modules.
- Kill in-flight container-events child processes on shutdown.
  spawn() shares the parent's process group by default, so a
  terminal Ctrl+C's whole-group SIGINT takes them along for free, but
  a direct SIGTERM to just this PID does not, and process.exit()
  prevents waitForContainerEvent's own JS-side timeout from ever
  running to kill them itself. Track spawned children in a
  module-level set; shutdown() kills everything still in it. Added a
  SIGTERM counterpart to the existing SIGINT test.
- Log a warning instead of silently proceeding when the events
  subprocess errors or exits with an unexpected non-zero code,
  instead of treating it the same as a successful match.
- Run update --watch's first cycle immediately instead of leaving the
  current tree undeployed until the first save, matching start
  --watch's build-then-watch structure. This also subsumes the
  previous round's standalone ensureRuntimeRunning pre-flight call.

Manually verified end to end: scaffolded a backend plugin with
plugin new, ran plugin dev start and plugin dev update --watch
against a real RHDH Local checkout (podman). Confirmed the initial
--watch deploy happens before entering watch mode, a source edit
triggers a full re-export/re-stage/restart cycle, tsconfig.json is
listed in the watch log, and SIGTERM sent mid-cycle kills the
in-flight settle-wait's podman events children with no orphans left
behind (verified via ps). The new close-code warning also fired for
real during this session against a live podman events subprocess,
confirming it surfaces genuine environment hiccups rather than only
synthetic test scenarios.

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
Assisted-By: opencode

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
- typescript:S6544: `if (settlePromise) await settlePromise;` used a
  Promise value directly as a boolean condition. Changed to an
  explicit `!== undefined` check.
- Duplication gate: the two new settle-wait tests (holds running
  through the settle wait / kills an in-flight settle-wait
  subscription on shutdown) shared a near-identical block that put
  watchUpdate into the settle-wait phase (capture spawned children,
  hold cycle 1 open, inject a second change, release, wait for both
  events subscriptions to spawn). Extracted enterSettleWait(),
  resolveSettleWait(), and restoreDefaultSpawnMock() helpers; both
  tests now call them instead of repeating the setup.

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
Assisted-By: opencode

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@gashcrumb

Copy link
Copy Markdown
Member Author

All 7 findings from this review round addressed in 650e2a4 (SonarCloud follow-up in e8b503f):

  • settle-wait serialization + late subscription (command.ts:539): fixed — running held through the settle-wait; subscription moved before the compose calls it's waiting on.
  • Docker events shape (command.ts:375): fixed — Actor.Attributes fallback added; fixed the prior test that had (correctly, per your catch) been using Podman's shape for a Docker scenario.
  • build config not watched (command.ts:504): fixed — tsconfig.json added, per RHIDP-16673's acceptance criteria.
  • isIgnoredWatchPath absolute-path bug (command.ts:448): fixed — scoped to the plugin root via path.relative.
  • orphaned events children on SIGTERM (command.ts:582): fixed — tracked in a Set, killed on shutdown.
  • silent events-command failures (command.ts:394): fixed — warnings logged on error/unexpected close.
  • update --watch skips initial deploy (command.ts:306): fixed — runs one cycle immediately, matching start.

Manual end-to-end verification: scaffolded a backend plugin with plugin new, ran plugin dev start and plugin dev update --watch against a real RHDH Local checkout on podman. Confirmed the initial --watch deploy happens before entering watch mode, a source edit triggers a full cycle, tsconfig.json shows in the watch log, and kill -TERM sent directly to the CLI's PID mid-cycle (not a terminal Ctrl+C) killed the in-flight settle-wait's podman events children with no orphans left behind. The new close-code warning also fired for real against a live subprocess during this session, confirming it surfaces genuine environment hiccups.

A SonarCloud follow-up (e8b503f) fixed a Promise-in-boolean-conditional finding and de-duplicated two new tests that shared setup — quality gate is green with zero open issues.

All 64 unit tests, lint, prettier, and tsc pass.

@PatAKnight PatAKnight left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some incredibly minor nits, up to you if you want to include or not. Otherwise, LGTM

Comment thread AGENTS.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread README.md Outdated
Comment thread src/commands/dev/command.ts Outdated
Comment thread src/commands/dev/command.ts Outdated
…fallback

- Support lowercase status field on container events as an action
  fallback in waitForContainerEvent and add unit test coverage.
- Update comment in scheduleUpdate to reflect the while-loop cycle
  draining mechanism instead of referring to a stale finally block.
- Include tsconfig.json in watched files lists across AGENTS.md,
  CHANGELOG.md, and README.md.

Assisted-By: opencode
Signed-off-by: Stan Lewis <gashcrumb@gmail.com>

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
…2.1 profile

Transitive resolution of @backstage/cli-defaults 0.1.6+ by @backstage/cli
pulls in @backstage/cli-module-package-manager-yarn, which contains an
unresolved Yarn patch locator
(got@patch:got@npm%3A11.8.2#~/.yarn/patches/got-npm-11.8.2-c1eb105458.patch)
causing yarn install to fail with ENOENT in scaffolded standalone plugin
projects.

Pinning @backstage/cli-defaults to 0.1.5 in resolutions prevents this
transitive resolution and restores clean yarn install in generated projects.

Assisted-By: opencode
Signed-off-by: Stan Lewis <gashcrumb@gmail.com>

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@sonarqubecloud

Copy link
Copy Markdown

@PatAKnight PatAKnight left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@gashcrumb
gashcrumb merged commit 5be8184 into redhat-developer:main Sep 28, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/moderate PR risk: moderate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants