v0.279.0: after_install (decision 45), known default logins on the page, Part D empty-backup alarm, night chain (R-705), R-706
gates / gates (push) Successful in 27s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-28 18:38:30 +02:00
parent 2e9a948cbd
commit 0c702f834a
35 changed files with 1119 additions and 16 deletions
@@ -0,0 +1,61 @@
package main
import (
"go/ast"
"go/parser"
"go/token"
"testing"
)
// v0.279.0 (decision 45): the deploy-done hook — the ONE place a fresh install ends — runs after_install, and
// the hook is set on every box, not only inside `if notifier != nil` (a box with no hub still installs apps).
// COMPANION RED-PROOF: move SetDeployDoneHook back inside `if notifier != nil` → the second assertion fails;
// drop the RunAfterInstall call → the first fails.
func TestAfterInstall_IsWiredToEveryFreshInstall(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
var hook *ast.CallExpr
var insideNotifierIf bool
var walk func(n ast.Node, underNotifier bool)
walk = func(n ast.Node, underNotifier bool) {
ast.Inspect(n, func(x ast.Node) bool {
if ifs, ok := x.(*ast.IfStmt); ok {
if be, ok := ifs.Cond.(*ast.BinaryExpr); ok {
if id, ok := be.X.(*ast.Ident); ok && id.Name == "notifier" && be.Op == token.NEQ {
walk(ifs.Body, true)
if ifs.Else != nil {
walk(ifs.Else, underNotifier)
}
return false
}
}
}
if c, ok := x.(*ast.CallExpr); ok {
if s, ok := c.Fun.(*ast.SelectorExpr); ok && s.Sel.Name == "SetDeployDoneHook" && hook == nil {
hook, insideNotifierIf = c, underNotifier
}
}
return true
})
}
walk(f, false)
if hook == nil {
t.Fatal("SetDeployDoneHook is never called")
}
calls := false
ast.Inspect(hook, func(x ast.Node) bool {
if s, ok := x.(*ast.SelectorExpr); ok && s.Sel.Name == "RunAfterInstall" {
calls = true
}
return true
})
if !calls {
t.Fatal("the deploy-done hook does not run RunAfterInstall")
}
if insideNotifierIf {
t.Fatal("SetDeployDoneHook is set only when a notifier exists — a box with no hub would never run after_install")
}
}
+34 -10
View File
@@ -1191,6 +1191,18 @@ func main() {
// It rides the EXISTING per-run digest (backup_run_failures) rather than a new event type: a
// new type is a two-repo change (the hub's allowedEventTypes drops anything unlisted), and the
// digest is already operator-only and already means "this run did not fully do its job".
// Part D (v0.279.0): a RUNNING app whose newest copy holds no database dump and no volume tar. Rides
// the same operator-only digest as R-203 (no new event type — the hub drops unlisted ones), once per
// app per tier per day (the backup package dedupes).
backupMgr.SetHollowCopyNotify(func(app, tier string) {
d := notify.BackupRunFailuresDetails{RunKind: "data-check", Attempted: 1, Failed: 1,
Apps: []notify.RunFailureDetail{{App: app, Leg: "hollow-" + tier,
Reason: "the app is running but its newest " + tier + " copy holds no database dump and no volume tar"}}}
notifier.NotifyBackupRunFailures(fmt.Sprintf(
"Backup holds NO DATA: %s is running, but its newest %s copy carries no database dump and no volume tar — "+
"the app is not protected on that tier. Check for a hold on a running app (R-704's shape) or a skipped dump.",
app, tier), d)
})
backupMgr.SetOffboxGapNotify(func(gaps map[string][]string) {
apps := make([]string, 0, len(gaps))
for app := range gaps {
@@ -1749,19 +1761,31 @@ func main() {
// R-536: „Alkalmazás telepítve" is sent when the async deploy ACTUALLY finishes, and a deploy
// that ends badly now says so instead of being silence. The accept-time event is
// `app_deploy_started`, emitted by the API router beside its 202.
if notifier != nil {
stackMgr.SetDeployDoneHook(func(name string, ok bool, detail string) {
display := name
if s, found := stackMgr.GetStack(name); found && s.Meta.DisplayName != "" {
display = s.Meta.DisplayName
}
if ok {
// v0.279.0: the hook is set on EVERY box — after_install (decision 45) must run on a box with no hub too;
// only the events need the notifier.
stackMgr.SetDeployDoneHook(func(name string, ok bool, detail string) {
display := name
if s, found := stackMgr.GetStack(name); found && s.Meta.DisplayName != "" {
display = s.Meta.DisplayName
}
if ok {
if notifier != nil {
notifier.NotifyAppDeployed(name, display)
return
}
// v0.279.0 (decision 45): a FRESH install replaces a known default login with its generated one,
// when the template declares after_install. Never on a restore or a kept-data load — those do not
// pass through the deploy-done hook.
go func() {
if ran, err := stackMgr.RunAfterInstall(name, 10*time.Minute); ran && err != nil {
log.Printf("[WARN] [stacks] after_install %s: %v", name, err)
}
}()
return
}
if notifier != nil {
notifier.NotifyAppDeployFailed(name, display, detail)
})
}
}
})
// R-681 (v0.270.0): an install a restart cut off is finished through the failure path and reported —
// AFTER the deploy-done hook exists, so its event is sent.
if names := stackMgr.RecoverInterruptedInstalls(); len(names) > 0 {