"Extract the decision so the suite can call it" pins the decision and never the wiring — drain_gate_refusal's call site can be bypassed with the suite green #528

Closed
opened 2026-09-12 07:01:05 +02:00 by ltms · 2 comments
Owner

Found while adjudicating #526 (fleetd #521). The fix I have prescribed three times in this file is incomplete, and this is my defect, not any implementer's. I measured both halves myself before filing.

The remedy, and what is wrong with it

Three times now, for a branch in scripts/redeploy-fleetd.sh that no test could reach, the ticket said: extract the decision into a function so the suite can call it.

# extracted by
#510 wait_for_daemon_exit so its ordering became checkable
#517 / #520 drain_gate_refusal so its abort branch became checkable
#521 / #526 should_swap so the swap guard became checkable

Each time the new tests call the extracted function directly and pass. Each time nothing makes the main flow consult it. Extraction does not remove the untested decision; it moves it up one level, from "a branch no test can reach" to "a call site no test can reach". Coverage goes up while a new uncovered decision appears.

This is the same shape as the Java finding already recorded against Fleetd.main — extraction moved the untested surface up and five call sites survived — so it is not specific to shell.

Measured: #521's prescribed fix did not close #521

PR #526 did exactly what #521 asked: should_swap(do_build), plus a test for each value. With the main flow then reading if should_swap "$DO_BUILD"; then:

if should_swap "$DO_BUILD"; then   ->   if false; then
bash scripts/test-redeploy-fleetd.sh
exit=0
real FAIL lines: 0

Proven applied: mutant string present (1), original string absent (0), should_swap itself untouched (still defined). Restored byte-identical.

Harness proof on my own invocation, so the green is readable: inverting should_swap's own body gave exit=1 and FAIL: should_swap 1 (a build ran and staged a jar) must return true. The suite can fail when I run it. It does not fail for the call site.

That is fixed in #526 as merged — the decision and the action now live together in swap_if_built, which the main flow calls unconditionally, and two tests drive it with a recording stub. This ticket is about the other two.

1. drain_gate_refusal's call site is not pinned at all

scripts/redeploy-fleetd.sh:683:

die "$(drain_gate_refusal "$DO_BUILD" "$JAR_STAGED")"

Mutation — the main flow stops consulting it and goes back to the old flat message:

die "aborted — nothing changed"
exit=0
real FAIL lines: 0
output: 256 bytes — byte-identical to a clean run

Proven applied: mutant present (1), drain_gate_refusal "$DO_BUILD" absent (0), the function itself still defined (1). Restored byte-identical.

This reinstates the exact defect #517 was filed to fix, with every test #520 added still green. #520's tests prove the four cases of drain_gate_refusal produce the right strings. Nothing proves the abort path uses it. Note test_drain_gate_refusal_* and the kept source-text test at the --no-build claim both survive: the source-text test greps this script for the claim wording, and the wording is still in the function.

Severity: the same as #517's — a wrong abort message during an abort the operator chose. Lower than #521's silent wrong outcome, but it is a regression that a merge one day old already permits.

2. wait_for_daemon_exit's call site is pinned, but only by text

test_swap_ordered_after_wait_and_before_start greps for wait_for_daemon_exit "$STOP_WAIT" and compares line positions, so deleting the call is caught. That is a source-text pin, with the known limit: it proves what the file says, never what runs. I did not find a mutation that makes the call present-but-ineffective while keeping that needle, but I also did not search exhaustively — treat this row as "partially pinned, not audited", not as clear.

The fix

Do not add another extracted predicate. Use the shape that #526 ended up with:

  • Put the decision and the action in one function, so the main flow has no guard of its own to get wrong and calls one function unconditionally.
  • Test that function behaviourally — with a stub recording whether the action happened — for every value of the decision. That fails if the guard is removed, inverted, or stops being consulted.
  • Keep the predicate as its own named function and its own test when the decision is worth naming. Just do not let it be the only thing tested.

For drain_gate_refusal concretely: a refuse_drain_gate(do_build, staged_path) that composes the message and calls die itself, with the main flow calling that one function, and tests asserting the message the composite produces for each of the four cases.

Then state plainly in a comment what the remaining hole is — deleting the call line altogether — and which test covers it.

Also fix: the test that was supposed to report a missing call site could not

While measuring the above, deleting the swap call made the suite exit 1 having printed zero bytes — no FAIL: line, nothing naming what was missing. Cause, measured with a control:

swap_line="$(grep -Fn '<needle>' "$src" | head -1 | cut -d: -f1)"
[ -n "$swap_line" ] || fail "could not find the swap call site in redeploy-fleetd.sh"

The file runs under set -euo pipefail. pipefail makes the pipeline's status grep's status, so an absent needle fails the assignment and set -e kills the suite on the spot — before the guard written to report exactly that case. With pipefail off, the guard is reached and prints. So all three || fail "could not find …" guards in that test were dead code, in precisely the situation they existed for.

Already fixed in #526 (each grep now ends || true), and the fix is proven: the same deletion now reports FAIL: could not find the swap call site in redeploy-fleetd.sh. Flagged here because the pattern is likely elsewhere — any x="$(cmd | cmd)" followed by an emptiness check, in any script under set -e with pipefail, has a dead check. Sweep for it and report; do not fix outside this file in this ticket.

Acceptance

  • bash scripts/test-redeploy-fleetd.sh exits 0; report test functions defined and invoked, and they must match.
  • bash -n passes under both /bin/bash (3.2.57) and env bash (5.x).
  • The item 1 mutation is killed: replace the die "$(drain_gate_refusal …)" call with a flat die "aborted — nothing changed", show the suite red, quote the FAIL line and exit code, restore, confirm byte-identical with shasum -a 256, then a green control.
  • Prove each mutation applied with two greps using different search strings, plus a grep -n re-read of the line. Two traps, both of which have bitten me this week: a $-variable inside a double-quoted pattern is expanded by your own shell, and a \" left inside a single-quoted pattern stays in the pattern as a literal backslash. Both give a false zero that looks like an un-applied mutation. A genuinely un-applied mutation gives 0 and 1, never 0 and 0.
  • Include a harness proof on your own invocation — mutate something the suite already pins, show it red — so your green results are readable.
  • Sweep for the pipefail dead-check pattern described above and list what you find. Do not fix those here.
  • Do not run scripts/redeploy-fleetd.sh against the live daemon, with any flag. It is the channel the fleet talks through.

Related

  • #521 / #526 — where this was found; the should_swap half is fixed there.
  • #517 / #520 — the merge that item 1 can silently undo.
  • #512 — the third outstanding defect in this script. Holds the same file, so sequence against it.
  • #504 — four swallowed-failure sites in the same file, still untouched.
Found while adjudicating #526 (fleetd #521). **The fix I have prescribed three times in this file is incomplete, and this is my defect, not any implementer's.** I measured both halves myself before filing. ## The remedy, and what is wrong with it Three times now, for a branch in `scripts/redeploy-fleetd.sh` that no test could reach, the ticket said: *extract the decision into a function so the suite can call it.* | # | extracted | by | |---|---|---| | #510 | `wait_for_daemon_exit` | so its ordering became checkable | | #517 / #520 | `drain_gate_refusal` | so its abort branch became checkable | | #521 / #526 | `should_swap` | so the swap guard became checkable | Each time the new tests call the extracted function directly and pass. Each time **nothing makes the main flow consult it.** Extraction does not remove the untested decision; it moves it up one level, from "a branch no test can reach" to "a call site no test can reach". Coverage goes up while a new uncovered decision appears. This is the same shape as the Java finding already recorded against `Fleetd.main` — extraction moved the untested surface up and five call sites survived — so it is not specific to shell. ## Measured: #521's prescribed fix did not close #521 PR #526 did exactly what #521 asked: `should_swap(do_build)`, plus a test for each value. With the main flow then reading `if should_swap "$DO_BUILD"; then`: ``` if should_swap "$DO_BUILD"; then -> if false; then ``` ``` bash scripts/test-redeploy-fleetd.sh exit=0 real FAIL lines: 0 ``` Proven applied: mutant string present (1), original string absent (0), `should_swap` itself untouched (still defined). Restored byte-identical. **Harness proof on my own invocation**, so the green is readable: inverting `should_swap`'s own body gave `exit=1` and `FAIL: should_swap 1 (a build ran and staged a jar) must return true`. The suite can fail when I run it. It does not fail for the call site. That is fixed in #526 as merged — the decision and the action now live together in `swap_if_built`, which the main flow calls unconditionally, and two tests drive it with a recording stub. **This ticket is about the other two.** ## 1. `drain_gate_refusal`'s call site is not pinned at all `scripts/redeploy-fleetd.sh:683`: ```bash die "$(drain_gate_refusal "$DO_BUILD" "$JAR_STAGED")" ``` Mutation — the main flow stops consulting it and goes back to the old flat message: ```bash die "aborted — nothing changed" ``` ``` exit=0 real FAIL lines: 0 output: 256 bytes — byte-identical to a clean run ``` Proven applied: mutant present (1), `drain_gate_refusal "$DO_BUILD"` absent (0), the function itself still defined (1). Restored byte-identical. **This reinstates the exact defect #517 was filed to fix**, with every test #520 added still green. #520's tests prove the four cases of `drain_gate_refusal` produce the right strings. Nothing proves the abort path uses it. Note `test_drain_gate_refusal_*` and the kept source-text test at the `--no-build` claim both survive: the source-text test greps this script for the claim wording, and the wording is still in the function. Severity: the same as #517's — a wrong abort message during an abort the operator chose. Lower than #521's silent wrong outcome, but it is a *regression that a merge one day old already permits*. ## 2. `wait_for_daemon_exit`'s call site is pinned, but only by text `test_swap_ordered_after_wait_and_before_start` greps for `wait_for_daemon_exit "$STOP_WAIT"` and compares line positions, so deleting the call is caught. That is a source-text pin, with the known limit: it proves what the file *says*, never what runs. I did not find a mutation that makes the call present-but-ineffective while keeping that needle, but I also did not search exhaustively — treat this row as "partially pinned, not audited", not as clear. ## The fix **Do not add another extracted predicate.** Use the shape that #526 ended up with: - Put the decision and the action in one function, so the main flow has no guard of its own to get wrong and calls one function unconditionally. - Test that function behaviourally — with a stub recording whether the action happened — for every value of the decision. That fails if the guard is removed, inverted, **or stops being consulted**. - Keep the predicate as its own named function and its own test when the decision is worth naming. Just do not let it be the only thing tested. For `drain_gate_refusal` concretely: a `refuse_drain_gate(do_build, staged_path)` that composes the message and calls `die` itself, with the main flow calling that one function, and tests asserting the message the composite produces for each of the four cases. Then state plainly in a comment what the remaining hole is — deleting the call line altogether — and which test covers it. ## Also fix: the test that was supposed to report a missing call site could not While measuring the above, deleting the swap call made the suite exit 1 having printed **zero bytes** — no `FAIL:` line, nothing naming what was missing. Cause, measured with a control: ```bash swap_line="$(grep -Fn '<needle>' "$src" | head -1 | cut -d: -f1)" [ -n "$swap_line" ] || fail "could not find the swap call site in redeploy-fleetd.sh" ``` The file runs under `set -euo pipefail`. `pipefail` makes the pipeline's status `grep`'s status, so an absent needle fails the assignment and `set -e` kills the suite on the spot — before the guard written to report exactly that case. With `pipefail` off, the guard is reached and prints. So all three `|| fail "could not find …"` guards in that test were dead code, in precisely the situation they existed for. Already fixed in #526 (each grep now ends `|| true`), and the fix is proven: the same deletion now reports `FAIL: could not find the swap call site in redeploy-fleetd.sh`. **Flagged here because the pattern is likely elsewhere** — any `x="$(cmd | cmd)"` followed by an emptiness check, in any script under `set -e` with `pipefail`, has a dead check. Sweep for it and report; do not fix outside this file in this ticket. ## Acceptance - `bash scripts/test-redeploy-fleetd.sh` exits 0; report test functions defined and invoked, and they must match. - `bash -n` passes under **both** `/bin/bash` (3.2.57) and `env bash` (5.x). - **The item 1 mutation is killed**: replace the `die "$(drain_gate_refusal …)"` call with a flat `die "aborted — nothing changed"`, show the suite red, quote the FAIL line and exit code, restore, confirm byte-identical with `shasum -a 256`, then a green control. - Prove each mutation applied with two greps using **different** search strings, plus a `grep -n` re-read of the line. Two traps, both of which have bitten me this week: a `$`-variable inside a *double*-quoted pattern is expanded by your own shell, and a `\"` left inside a *single*-quoted pattern stays in the pattern as a literal backslash. Both give a false zero that looks like an un-applied mutation. A genuinely un-applied mutation gives 0 and 1, never 0 and 0. - Include a **harness proof on your own invocation** — mutate something the suite already pins, show it red — so your green results are readable. - Sweep for the `pipefail` dead-check pattern described above and **list** what you find. Do not fix those here. - Do **not** run `scripts/redeploy-fleetd.sh` against the live daemon, with any flag. It is the channel the fleet talks through. ## Related - #521 / #526 — where this was found; the `should_swap` half is fixed there. - #517 / #520 — the merge that item 1 can silently undo. - #512 — the third outstanding defect in this script. Holds the same file, so sequence against it. - #504 — four swallowed-failure sites in the same file, still untouched.
Author
Owner

Attribution correction to the ticket body above.

I wrote that I measured the call-site gap. I did, but #526's implementer found it first and reported it in that PR's body under "Caveat for review" — including that the same residual gap already existed on #517's drain_gate_refusal precedent, which is item 1 of this ticket. I reproduced it independently before reading the body, so it is measured twice by two routes, but the finding was not mine. Full quote and credit in #526 (comment).

Two corrections to this ticket's own content that follow from that report:

Severity of the should_swap case was overstated in #521, and I am carrying that error into this ticket's framing. #521 called it a "silent wrong outcome during a redeploy the operator believes succeeded". The implementer traced it: stage_built_jar is unconditional, so by the swap step $JAR is already absent, java -jar fails at once, and the script dies at no process appeared or /healthz never answered within ${HEALTH_WAIT}s — both of which print the daemon log tail. It is a loud failure, not a silent one. The defect is still real (a guard that can be disabled with a green suite), but "reports success" was wrong. That trace was not run against the live daemon and I have not re-verified it.

This does not change item 1's severity. drain_gate_refusal produces a wrong message on a path that still completes, so there is no loud failure to save it.

A larger list exists and is not in this ticket. The sweep #521 asked for turned up only the two source-text-only tests already known. But the implementer also listed seven main-flow decisions past the SOURCED guard with no test of any kind — the three case "$SUPERVISOR_KIND" dispatches (report display, stop, start), if [ "$CHECK_ONLY" = 1 ], the drain-gate's own entry condition and the if [ "$reply" != "yes" ] die inside it, if [ -z "$HEALTH_BODY" ], and the ok/warn summary. Those are listed in the comment linked above, with pre-merge line numbers that need re-measuring.

Whoever takes this ticket: do not widen into those seven. They are a bigger piece of work and the stop dispatch in particular deserves its own ticket. Stay on the two items here.

Attribution correction to the ticket body above. I wrote that I measured the call-site gap. I did, but **#526's implementer found it first** and reported it in that PR's body under "Caveat for review" — including that the same residual gap already existed on #517's `drain_gate_refusal` precedent, which is item 1 of this ticket. I reproduced it independently before reading the body, so it is measured twice by two routes, but the finding was not mine. Full quote and credit in https://git.ltms.dev/fleet/fleetd/pulls/526#issuecomment-16613. Two corrections to this ticket's own content that follow from that report: **Severity of the `should_swap` case was overstated in #521, and I am carrying that error into this ticket's framing.** #521 called it a "silent wrong outcome during a redeploy the operator believes succeeded". The implementer traced it: `stage_built_jar` is unconditional, so by the swap step `$JAR` is already absent, `java -jar` fails at once, and the script dies at `no process appeared` or `/healthz never answered within ${HEALTH_WAIT}s` — both of which print the daemon log tail. It is a **loud** failure, not a silent one. The defect is still real (a guard that can be disabled with a green suite), but "reports success" was wrong. That trace was not run against the live daemon and I have not re-verified it. This does not change item 1's severity. `drain_gate_refusal` produces a *wrong message* on a path that still completes, so there is no loud failure to save it. **A larger list exists and is not in this ticket.** The sweep #521 asked for turned up only the two source-text-only tests already known. But the implementer also listed **seven main-flow decisions past the `SOURCED` guard with no test of any kind** — the three `case "$SUPERVISOR_KIND"` dispatches (report display, stop, start), `if [ "$CHECK_ONLY" = 1 ]`, the drain-gate's own entry condition and the `if [ "$reply" != "yes" ]` die inside it, `if [ -z "$HEALTH_BODY" ]`, and the ok/warn summary. Those are listed in the comment linked above, with pre-merge line numbers that need re-measuring. Whoever takes this ticket: do **not** widen into those seven. They are a bigger piece of work and the stop dispatch in particular deserves its own ticket. Stay on the two items here.
Author
Owner

Fixed in PR #532, merged. Closing.

Item 1 — fixed and pinned two ways

refuse_drain_gate composes the message via drain_gate_refusal and calls die itself; the main flow calls it unconditionally at :710. Verified in my own worktree at the pushed head 7c34e8f, independently of the worker's report.

Cell Result
clean control exit 0, 0 lines matching ^FAIL:, 256 bytes
call site deleted (:710 → flat die "aborted — nothing changed") exit 1, 1 ^FAIL: line, 87 bytes — FAIL: could not find the main flow's refuse_drain_gate call site in redeploy-fleetd.sh
refuse_drain_gate stops consulting the predicate exit 1, 1 ^FAIL: line — FAIL: refuse_drain_gate build-ran+staged-present die message does not name the staged jar
restored byte-identical 0e5a99a22c9c65f72960d8f179ca5299307889e06bc42131f098a513e7b97bd6, control green
bash -n rc=0 on both files, under both /bin/bash 3.2.57 and env bash 5.3.9
test functions defined / invoked 49 / 49 (was 44/44)
CI run 1781 success

The first cell is the whole point of this ticket. Before the change, that same mutation gave exit 0, zero FAIL lines, and output byte-identical to a clean run. The regression window #517 had reopened is closed, and closed in a way that reports rather than passes silently.

Each mutation was proven applied with a uniquely tagged marker plus a second different search string, with a control against a pristine copy showing the exact inverse, and the function definition confirmed still present so the mutation hit the call and not the function.

The self-match trap, caught by the implementer

The worker's first draft of the comment above refuse_drain_gate quoted the call-site string literally. That would have given the source-text test a second match — in the comment — so the test would have passed with the real call deleted. They caught it themselves and reworded the comment to avoid the literal. grep -cF 'refuse_drain_gate "$DO_BUILD" "$JAR_STAGED"' now returns 1, at :710.

That is the same family as a probe grepping a file that contains its own needle, which cost me a false pass earlier today. Catching it unprompted is the better half of this PR, and worth more than the fix.

The sweep — a real negative, with reasoning

No live instances of the pipefail dead-check pattern. Of the five scripts under set -e with pipefail, every var="$(cmd | cmd)" already carries || true or || echo; the remaining two have no pipe-into-assignment at all. probe-member-credentials.sh (set -uo pipefail, deliberately no -e) and deploy/herdr-inner.sh (no set -e) were correctly excluded rather than silently counted.

A negative result with the exclusions named is worth recording, because the next person will otherwise re-run the sweep.

What stays open, and where

Closing this ticket does not close these — stated explicitly, because a ticket with several items is otherwise closed by the first one that produces a green run:

  • Item 2, wait_for_daemon_exit's call site — unchanged, and still exactly what this ticket said it was: source-text pinned only, "partially pinned, not audited." I did not find a mutation that makes the call present-but-ineffective while keeping the needle, and I did not search exhaustively. Nobody should read this closure as that row being clear.
  • The seven untested main-flow decisions (CHECK_ONLY, the drain-gate's own entry if and its reply != yes condition, the three case "$SUPERVISOR_KIND" dispatches, HEALTH_BODY, the error-count summary) — untouched. This PR changed the body of one of those if blocks while leaving the guard condition itself untested, as instructed. Line numbers shifted in this merge, so they need re-measuring before anyone ticket them.
  • #512 part 2 — the script-side positive assertion of the drain-complete line plus the shape grep for ^Exception in thread and NoClassDefFoundError. It was sequenced behind this ticket because they share both files. That file is now free and #512 part 2 is next.

One inaccuracy, in the report only

The worker's report abbreviates the restored hash as 0e5a99a2...78f0a. That tail does not occur in the real hash, which ends b97bd6. I hashed the committed file myself and confirmed the restore matched, so the file is correct and only the quoted abbreviation is wrong. Flagged because an abbreviated hash nobody can match against anything is worse than quoting none.

Fixed in PR #532, merged. Closing. ## Item 1 — fixed and pinned two ways `refuse_drain_gate` composes the message via `drain_gate_refusal` and calls `die` itself; the main flow calls it unconditionally at `:710`. Verified in my own worktree at the pushed head `7c34e8f`, independently of the worker's report. | Cell | Result | |---|---| | clean control | exit 0, 0 lines matching `^FAIL:`, 256 bytes | | **call site deleted** (`:710` → flat `die "aborted — nothing changed"`) | **exit 1, 1 `^FAIL:` line, 87 bytes** — `FAIL: could not find the main flow's refuse_drain_gate call site in redeploy-fleetd.sh` | | **`refuse_drain_gate` stops consulting the predicate** | **exit 1, 1 `^FAIL:` line** — `FAIL: refuse_drain_gate build-ran+staged-present die message does not name the staged jar` | | restored | byte-identical `0e5a99a22c9c65f72960d8f179ca5299307889e06bc42131f098a513e7b97bd6`, control green | | `bash -n` | rc=0 on both files, under both `/bin/bash` 3.2.57 and `env bash` 5.3.9 | | test functions defined / invoked | 49 / 49 (was 44/44) | | CI run 1781 | success | **The first cell is the whole point of this ticket.** Before the change, that same mutation gave exit 0, zero FAIL lines, and output byte-identical to a clean run. The regression window #517 had reopened is closed, and closed in a way that *reports* rather than passes silently. Each mutation was proven applied with a uniquely tagged marker plus a second different search string, with a control against a pristine copy showing the exact inverse, and the function definition confirmed still present so the mutation hit the call and not the function. ## The self-match trap, caught by the implementer The worker's first draft of the comment above `refuse_drain_gate` quoted the call-site string literally. That would have given the source-text test a second match — in the comment — so the test would have passed with the real call deleted. They caught it themselves and reworded the comment to avoid the literal. `grep -cF 'refuse_drain_gate "$DO_BUILD" "$JAR_STAGED"'` now returns 1, at `:710`. That is the same family as a probe grepping a file that contains its own needle, which cost me a false pass earlier today. Catching it unprompted is the better half of this PR, and worth more than the fix. ## The sweep — a real negative, with reasoning No live instances of the `pipefail` dead-check pattern. Of the five scripts under `set -e` with `pipefail`, every `var="$(cmd | cmd)"` already carries `|| true` or `|| echo`; the remaining two have no pipe-into-assignment at all. `probe-member-credentials.sh` (`set -uo pipefail`, deliberately no `-e`) and `deploy/herdr-inner.sh` (no `set -e`) were correctly excluded rather than silently counted. A negative result with the exclusions named is worth recording, because the next person will otherwise re-run the sweep. ## What stays open, and where Closing this ticket does **not** close these — stated explicitly, because a ticket with several items is otherwise closed by the first one that produces a green run: - **Item 2, `wait_for_daemon_exit`'s call site** — unchanged, and still exactly what this ticket said it was: *source-text pinned only, "partially pinned, not audited."* I did not find a mutation that makes the call present-but-ineffective while keeping the needle, and I did not search exhaustively. Nobody should read this closure as that row being clear. - **The seven untested main-flow decisions** (`CHECK_ONLY`, the drain-gate's own entry `if` and its `reply != yes` condition, the three `case "$SUPERVISOR_KIND"` dispatches, `HEALTH_BODY`, the error-count summary) — untouched. This PR changed the *body* of one of those `if` blocks while leaving the guard condition itself untested, as instructed. Line numbers shifted in this merge, so they need re-measuring before anyone ticket them. - **#512 part 2** — the script-side positive assertion of the drain-complete line plus the shape grep for `^Exception in thread` and `NoClassDefFoundError`. It was sequenced behind this ticket because they share both files. That file is now free and #512 part 2 is next. ## One inaccuracy, in the report only The worker's report abbreviates the restored hash as `0e5a99a2...78f0a`. That tail does not occur in the real hash, which ends `b97bd6`. I hashed the committed file myself and confirmed the restore matched, so the file is correct and only the quoted abbreviation is wrong. Flagged because an abbreviated hash nobody can match against anything is worse than quoting none.
ltms closed this issue 2026-09-12 07:42:32 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#528