web: every dry-run reports a phantom bitborg-web restart (check-mode fabricates command results) #294

Stängd
öppnade 2026-07-31 22:06:42 +00:00 av supernaut · 0 kommentarer
Ägare

Summary

Every --check --diff dry-run of site.yml reports a bitborg-web restart that a real run does
not perform
. The web role's restart decision is computed from two command probes, and under
check mode those probes do not run — Ansible registers a fabricated result in their place, so
the decision is made from fiction.

Found while gating an unrelated docs-only change (#292/#293): the dry-run came back changed=1,
which looked like real pending drift and had to be chased down before the apply gate could be
cleared.

Mechanism

A skipped command under --check does not register "no data" — it registers plausible success:

{ "rc": 0, "stdout": "", "skipped": true,
  "msg": "Command would have run if not in check mode" }

Both halves of that are load-bearing here, in opposite directions:

flowchart TD
  C["--check skips both command probes"]
  C --> R["web_image_present.rc = 0<br/>(fabricated)"]
  C --> S["web_running_image.stdout = ''<br/>(fabricated)"]
  R --> RG["'is the image published?' guard<br/>always takes the yes branch"]
  S --> SG["'' != web_image:tag<br/>→ state = restarted"]
  SG --> P["phantom restart in every dry-run"]
  RG --> F["dry-run cannot surface an unpublished image"]
  • stdout: "" can never equal git.gitborg.se/gitborg/gitborg-web:latest, so the drift comparison
    always resolves state: restarted → a changed that never materialises.
  • rc: 0 means the when: web_image_present.rc == 0 guards take the success branch regardless, so
    a dry-run silently assumes the image is published even when it is not — the failure the
    "Note when the bitborg-web image is not yet published" task exists to report.

Note the existing | default('') guard on stdout is not what saves or breaks this — the value is
registered as "" outright, so the default never fires.

Why it matters

The apply procedure requires accounting for every changed task, precisely so that real drift
(a merged-but-unapplied PR) is not lost in noise. A permanent phantom changed erodes that: it
trains the reader to expect one unexplained change and wave it through. Verified against the host
that no restart was actually pending — the running container was already on the expected ref.

Fix

check_mode: false on the two probes. Both are strictly read-only (podman image exists,
podman container inspect) and already carry changed_when: false / failed_when: false, so
running them under check mode is safe and makes the dry-run honest.

systemctl --user reset-failed in the same block is deliberately left skipped — it mutates
state and has no business running during a dry-run.

Verification

Scoped dry-run (--check --diff --tags web) before and after:

Task Before After
Check the bitborg-web image is present skipping ok (really running)
Inspect the running bitborg-web image skipping ok (really running)
Enable and start bitborg-web changed ok
PLAY RECAP changed=1 changed=0

Possible follow-up

Other roles were checked and do not share the drift-comparison shape, so this is web-specific
today. The general trap is not: any when:/state expression derived from a command register is
reading fabricated values under --check. Worth a sweep for read-only probes that should carry
check_mode: false, and worth a line in the apply procedure so the next person recognises the
pattern instead of re-deriving it.

## Summary Every `--check --diff` dry-run of `site.yml` reports a **bitborg-web restart that a real run does not perform**. The web role's restart decision is computed from two `command` probes, and under check mode those probes do not run — Ansible registers a **fabricated result** in their place, so the decision is made from fiction. Found while gating an unrelated docs-only change (#292/#293): the dry-run came back `changed=1`, which looked like real pending drift and had to be chased down before the apply gate could be cleared. ## Mechanism A skipped `command` under `--check` does not register "no data" — it registers plausible success: ```json { "rc": 0, "stdout": "", "skipped": true, "msg": "Command would have run if not in check mode" } ``` Both halves of that are load-bearing here, in opposite directions: ```mermaid flowchart TD C["--check skips both command probes"] C --> R["web_image_present.rc = 0<br/>(fabricated)"] C --> S["web_running_image.stdout = ''<br/>(fabricated)"] R --> RG["'is the image published?' guard<br/>always takes the yes branch"] S --> SG["'' != web_image:tag<br/>→ state = restarted"] SG --> P["phantom restart in every dry-run"] RG --> F["dry-run cannot surface an unpublished image"] ``` - `stdout: ""` can never equal `git.gitborg.se/gitborg/gitborg-web:latest`, so the drift comparison always resolves `state: restarted` → a `changed` that never materialises. - `rc: 0` means the `when: web_image_present.rc == 0` guards take the success branch regardless, so a dry-run silently assumes the image is published even when it is not — the failure the "Note when the bitborg-web image is not yet published" task exists to report. Note the existing `| default('')` guard on `stdout` is not what saves or breaks this — the value is registered as `""` outright, so the default never fires. ## Why it matters The apply procedure requires accounting for **every** `changed` task, precisely so that real drift (a merged-but-unapplied PR) is not lost in noise. A permanent phantom `changed` erodes that: it trains the reader to expect one unexplained change and wave it through. Verified against the host that no restart was actually pending — the running container was already on the expected ref. ## Fix `check_mode: false` on the two probes. Both are strictly read-only (`podman image exists`, `podman container inspect`) and already carry `changed_when: false` / `failed_when: false`, so running them under check mode is safe and makes the dry-run honest. `systemctl --user reset-failed` in the same block is deliberately **left skipped** — it mutates state and has no business running during a dry-run. ## Verification Scoped dry-run (`--check --diff --tags web`) before and after: | Task | Before | After | | ------------------------------------------- | ---------- | ---------------- | | Check the bitborg-web image is present | `skipping` | `ok` (really running) | | Inspect the running bitborg-web image | `skipping` | `ok` (really running) | | Enable and start bitborg-web | `changed` | `ok` | | PLAY RECAP | `changed=1` | **`changed=0`** | ## Possible follow-up Other roles were checked and do not share the drift-comparison shape, so this is web-specific today. The general trap is not: any `when:`/state expression derived from a `command` register is reading fabricated values under `--check`. Worth a sweep for read-only probes that should carry `check_mode: false`, and worth a line in the apply procedure so the next person recognises the pattern instead of re-deriving it.
supernaut ändrade titeln från web: every dry-run reports a phantom gitborg-web restart (check-mode fabricates command results) till web: every dry-run reports a phantom bitborg-web restart (check-mode fabricates command results) 2026-08-03 09:58:26 +00:00
Logga in för att delta i denna konversation.
Ingen milstolpe
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-infra#294
Ingen beskrivning angiven.