When two apps request the same domain, the losing app is set to Phase=Failed and never recovers on its own.
On collision the App is set Phase=Failed and the reconcile returns {}, nil with no requeue (internal/controller/app_controller.go ~:310-324). When the conflicting app later releases the domain, the Ingress delete event maps to the other app (the handler uses the Ingress own labels), so this App is never re-enqueued; shouldRefreshFailedAppStatus only refreshes Failed status for build failures, so even the status pass is skipped. The app self-heals only at the next informer resync (hours) or a manual edit.
Fix: return a RequeueAfter on domain collision, or map Ingress deletions to apps carrying a DomainCollision condition so they re-reconcile when the domain frees up.
Found during a Fable 5 quality audit of the codebase (same audit that produced #433). Verified against the code at the current main.
When two apps request the same domain, the losing app is set to
Phase=Failedand never recovers on its own.On collision the App is set
Phase=Failedand the reconcile returns{}, nilwith no requeue (internal/controller/app_controller.go~:310-324). When the conflicting app later releases the domain, the Ingress delete event maps to the other app (the handler uses the Ingress own labels), so this App is never re-enqueued;shouldRefreshFailedAppStatusonly refreshes Failed status for build failures, so even the status pass is skipped. The app self-heals only at the next informer resync (hours) or a manual edit.Fix: return a
RequeueAfteron domain collision, or map Ingress deletions to apps carrying aDomainCollisioncondition so they re-reconcile when the domain frees up.Found during a Fable 5 quality audit of the codebase (same audit that produced #433). Verified against the code at the current main.