package main // `carlos apps build-probe` — set, on an app that already exists, the path // `carlos deploy` polls to learn which build is ACTUALLY running. // // The probe already existed and could only be stamped at claim time: // SetAppBuildProbe had exactly one caller, the create door. So every app made // before the flag existed, and every app whose author did not think to pass // it, was permanently stuck on header-watching — the X-Carlos-Version header // the edge stamps from the route row the moment adoption moves a symlink, // whether or not any process restarted. Which means the fix for "deploy said // live over the old binary" was out of reach of exactly the apps that had the // problem (discussion #13). import ( "context" "flag" "fmt" "log" "strings" ) const buildProbeUsage = `usage: carlos apps build-probe --app [--account ] [--account-id ] [--console ] an absolute path, no query, whose response body names the running build — "/healthz" serving "ok abc1234" is the shape. carlos deploy polls it instead of the X-Carlos-Version header, which reports the release adoption INTENDED rather than the build actually serving. - clear the probe, back to watching the header.` // runAppsBuildProbe stamps the probe path, or clears it. // // "-" is the clear sentinel, the same one `carlos route --unit` and // `--environment` use and for the same reason: "" is a LEGAL value of this // field — it means header-watching — so an empty argument cannot be told from // an argument nobody meant to pass. The doors that clear say so out loud. // // The stored value is echoed back and compared rather than assumed, the same // discipline `apps create` applies to this exact field. It is what the deploy // watch trusts; a write that half-landed would leave a deploy quietly // believing a header again, which is the whole bug. func runAppsBuildProbe(args []string) { fs := flag.NewFlagSet("apps build-probe", flag.ExitOnError) app := appFlag(fs, "app whose build probe to set") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account") consoleFlag := consoleOption(fs, "console to act through (default: the only logged-in one)") fs.Usage = flagUsage(fs, buildProbeUsage) fs.Parse(args) if strings.TrimSpace(*app) == "" || fs.NArg() != 1 { log.Fatal(buildProbeUsage) } if err := validAppName(strings.TrimSpace(*app)); err != nil { log.Fatalf("apps build-probe: %v", err) } probe := strings.TrimSpace(fs.Arg(0)) clearProbe := probe == "-" if clearProbe { probe = "" } else { if probe == "" { log.Fatal(`apps build-probe: an empty probe is refused — pass "-" to clear it back to header-watching, or name the path`) } // Checked here, before the call, the same client-side grammar gate // `apps create` applies: a shape the server is certain to refuse // should cost no round trip and reach nothing. if err := validBuildProbe(probe); err != nil { log.Fatalf("apps build-probe: %v", err) } } sess, err := resolveMode(*consoleFlag) if err != nil { log.Fatal(err) } if sess == nil { log.Fatal("apps build-probe works through a console, and this shell is in direct bucket mode\n" + "(CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR is set). The probe is console-tracked\n" + "state on the app row: log in instead — carlos auth login --console ") } acct := resolvedAccount(sess, acctRaw, acctIDRaw) got, err := sess.setBuildProbe(context.Background(), acct, strings.TrimSpace(*app), probe) if err != nil { log.Fatalf("apps build-probe: %v", err) } if got != probe { log.Fatalf("apps build-probe: did not stick (console echoed %q, want %q) — the probe is what "+ "carlos deploy trusts, so a write that half-landed is worse than none", got, probe) } if clearProbe { fmt.Fprintf(stdout, "cleared the build probe for %s — carlos deploy watches the X-Carlos-Version header again\n", strings.TrimSpace(*app)) fmt.Fprintf(stdout, " that header is stamped by the edge from the route row, so it reports the release adoption intended, not the build serving\n") return } fmt.Fprintf(stdout, "build probe for %s: %s\n", strings.TrimSpace(*app), got) fmt.Fprintf(stdout, " carlos deploy now asks the app's own process which build it is running, instead of trusting the router's header\n") }