# Upgrade role Upgrades all packages using the native package manager (`apk` or `apt`). ## Problem Tailscale may be updated during the upgrade. When its service restarts, the SSH tunnel drops and Ansible loses connectivity, causing a fatal `UNREACHABLE`. ## Solution — async + poll Instead of a blocking upgrade, we fire the job in the background and poll for completion, tolerating temporary unreachability. ```mermaid sequenceDiagram participant A as Ansible participant T as Target A->>T: apk upgrade (async, poll: 0) T-->>A: job ID: 42 Note over T: 🔄 upgrade en cours... A->>T: async_status jid=42 T-->>A: finished: false Note over A: FAILED-RETRYING (normal) A-->>A: sleep 10s A->>T: async_status jid=42 T-->>A: finished: false Note over A: FAILED-RETRYING (normal) A-->>A: sleep 10s A->>T: async_status jid=42 T-->>A: finished: true ✅ Note over A: ok ``` - **`async: 3600` + `poll: 0`** — fire and forget. Ansible gets a job ID and moves on. - **`async_status` with `until: finished`** — polls every 10s for up to 60 retries (~10 min). - **`ignore_unreachable: true`** — if Tailscale dropped SSH, Ansible doesn't crash. It retries until the host is back. - The `FAILED - RETRYING` messages are normal — they just mean the `until` condition isn't met yet. ## Idempotency A task with `async` + `poll: 0` always reports `changed: true` by default, even when nothing happened. To fix this: ```mermaid graph LR A["Fire: changed_when: false"] --> B["Package manager runs in background"] B --> C["Wait: polls every 10s"] C -->|finished| D["async_status reads real changed value"] D -->|"packages updated"| E["changed ✅"] D -->|"nothing to do"| F["ok"] ``` - The **fire** task has `changed_when: false` — it never claims change. - The **wait** task reads `changed_when: async_result.changed` — it propagates the real status from the package manager once the job finishes. Second run with nothing to upgrade → `changed=0` on all hosts.