Skip to content

feat: publish Obol CLI version for the frontend footer - #802

Open
HananINouman wants to merge 4 commits into
mainfrom
feat/frontend-obol-version
Open

feat: publish Obol CLI version for the frontend footer#802
HananINouman wants to merge 4 commits into
mainfrom
feat/frontend-obol-version

Conversation

@HananINouman

@HananINouman HananINouman commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Write obolVersion into obol-stack-config on stack up / tunnel sync and inject OBOL_STACK_VERSION into the frontend pod so the UI can show the running CLI release.

Pairs with ObolNetwork/obol-stack-front-end#460, which renders the value.

Summary

What changed:

  • internal/tunnel/tunnel.goSyncTunnelConfigMap now publishes obolVersion alongside tunnelURL, and quotes both values.
  • internal/tunnel/tunnel.go — new SyncStackConfigVersion writes obolVersion alone, for the stack-up path where no tunnel sync runs. It applies under its own server-side-apply field manager (obol-stack-version).
  • internal/stack/stack.gosyncDefaults exports OBOL_STACK_VERSION to helmfile and calls SyncStackConfigVersion after the infra deploy.
  • internal/update/update.goobol upgrade exports the same variable, so the rendered podspec matches what stack up produces.
  • internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl — new OBOL_STACK_VERSION env on the frontend container.

Why it matters:

The frontend footer currently shows only the front-end image tag. Users debugging a stack have no in-UI answer to "which obol CLI built this?". Two delivery paths are wired on purpose: the pod env is the fast path and the only one that works for a local pnpm dev frontend outside the cluster; the ConfigMap is the authoritative in-cluster path and is the one that survives obol upgrade.

Risk level: medium — touches obol stack up's ConfigMap publishing, which serviceoffer-controller and the frontend both read.

Commit under test: 7a0f31a9

Base branch: main

Scope

  • Code
  • Charts / manifests
  • Flows / QA scripts
  • Docs / skills
  • Images / dependencies
  • Other:

Review Notes

Field manager is load-bearing (fixed in a6058c91)

The first cut of SyncStackConfigVersion applied {obolVersion} under kubectl's default SSA field manager — the same manager SyncTunnelConfigMap uses for {tunnelURL, obolVersion}. Server-side apply deletes fields a manager previously owned but no longer sends, so every call silently pruned tunnelURL. Reproduced against a live k3d cluster in a scratch namespace:

apply {tunnelURL, obolVersion} -> {"obolVersion":"0.14.0","tunnelURL":"https://x.example.com"}
apply {obolVersion}            -> {"obolVersion":"0.15.0"}          # tunnelURL gone

Blast radius, had it shipped: SyncStackConfigVersion runs on every obol stack up, immediately after the block in syncDefaults that skips the cloudflared chart specifically to preserve a healthy quick tunnel's URL. Quick-tunnel stacks never recover it in that run, because the tunnel stays dormant and SyncTunnelConfigMap is not called again (stack.go only reaches it under st.IsPersistent()). Downstream:

  • serviceoffercontroller.registrationBaseURL falls back to defaultBaseURL → ERC-8004 registration doc and the OpenAPI servers block lose the public origin.
  • Frontend getTunnelURL() → agent registration card and marketplace service URLs lose it too.
  • enqueueDiscoveryRefresh watches this exact ConfigMap, so even persistent-tunnel stacks (where it is restored later in the same run) re-render every offer and registration twice per stack up.

Fixed by applying under --field-manager=obol-stack-version. tunnelURL stays owned by kubectl and untouched; a later SyncTunnelConfigMap still overwrites both via --force-conflicts. Locked in by TestStackConfigVersionApplyArgs_UsesDedicatedFieldManager.

Reviewer focus

  • stackConfigVersionApplyArgs — the dedicated field manager is not cosmetic; see above.
  • internal/update/update.go — without the env line, obol upgrade renders OBOL_STACK_VERSION="", the podspec differs from stack up's, and the frontend deployment rolls for nothing.
  • Removing the kubectl create namespace --dry-run=client -o yaml call is safe: it never created anything (dry-run + -o yaml to a discarded stdout), and base/templates/obol-frontend.yaml pre-creates the namespace.

Known gaps

  • The feature is not user-visible until the frontend image pin is bumped. obol-frontend.yaml.gotmpl digest-pins v0.1.28-rc3; the footer that reads this data ships in obol-stack-front-end#460. That bump is not in this PR.
  • Dev builds show obol-dev. justfile passes ldflags for github.com/obol/obol-stack/internal/version, but the module is github.com/ObolNetwork/obol-stack — Go silently ignores the unknown symbol, so just build leaves Version = "dev". Verified locally (obol versionVersion: dev). .goreleaser.yaml uses the correct path, so released binaries report a real version. Pre-existing; worth a separate one-line fix, noting that the VERSION file also still reads 0.0.0.
  • Two write paths and two read paths is more surface than the minimum. The ConfigMap alone would suffice in-cluster and updates without rolling the pod; the env var earns its place only as the outside-the-cluster escape hatch for pnpm dev. Kept deliberately, documented in the front-end .env.example.

Follow-ups

Validation

CI checks:

Check Status Link
Go build, vet, and unit tests pass run 30127068462 (re-running on 7a0f31a9)
helm template embedded chart pass run 30127068532
CRD generation up-to-date pass run 30127068462
goreleaser config valid pass run 30127068462
lint-test / gitleaks / CodeQL pass runs 30127068462, 30127068738, 30127064575

Unit tests:

go build ./...                                              ok
go vet ./internal/tunnel ./internal/update ./internal/stack  ok
go test ./internal/tunnel ./internal/update ./internal/stack ./cmd/obol -count=1
  ok  internal/tunnel  2.771s
  ok  internal/update  1.862s
  ok  internal/stack   6.611s
  ok  cmd/obol         3.206s
commit 7a0f31a9

Cluster behaviour check (live k3d, throwaway namespace) — the SSA prune above, and the fix:

# shared field manager (pre-fix shape)
apply {tunnelURL, obolVersion} -> {"obolVersion":"0.14.0","tunnelURL":"https://x.example.com"}
apply {obolVersion}            -> {"obolVersion":"0.15.0"}

# dedicated field manager (shipped shape)
apply {tunnelURL, obolVersion}                              -> {"obolVersion":"0.14.0","tunnelURL":"https://x.example.com"}
apply {obolVersion} --field-manager=obol-stack-version      -> {"obolVersion":"0.15.0","tunnelURL":"https://x.example.com"}
apply {tunnelURL, obolVersion} --force-conflicts            -> {"obolVersion":"0.15.0","tunnelURL":"https://y.example.com"}

Integration tests:

not run

Flow tests:

Flow Network QA machine label Worktree Result Artifacts
not run

Release smoke:

not run

Recommended before merge: one obol stack up on a stack with a live quick tunnel, asserting obol kubectl get cm obol-stack-config -n obol-frontend -o jsonpath='{.data}' still carries both tunnelURL and obolVersion afterwards.

Live Chain Evidence

Not applicable — no on-chain surface in this change.

HananINouman and others added 2 commits July 24, 2026 19:03
Write obolVersion into obol-stack-config on stack up / tunnel sync and
inject OBOL_STACK_VERSION into the frontend pod so the UI can show the
running CLI release.

Co-authored-by: Cursor <cursoragent@cursor.com>
Collapse SyncStackConfigVersion to a single server-side apply of
obolVersion, quote ConfigMap data values, and drop the dry-run-only
namespace create that never applied.

Co-authored-by: Cursor <cursoragent@cursor.com>
@HananINouman
HananINouman requested a review from OisinKyne July 24, 2026 21:15
bussyjd added 2 commits July 28, 2026 14:46
SyncStackConfigVersion applied an obolVersion-only manifest under kubectl's
default server-side-apply field manager — the same manager SyncTunnelConfigMap
uses for {tunnelURL, obolVersion}. Server-side apply deletes fields a manager
previously owned but no longer sends, so every call dropped tunnelURL.

Reproduced against a live k3d cluster:

  apply {tunnelURL, obolVersion} -> {"obolVersion":"0.14.0","tunnelURL":"https://x.example.com"}
  apply {obolVersion}            -> {"obolVersion":"0.15.0"}

Blast radius: SyncStackConfigVersion runs on every `obol stack up`, directly
after the block that skips the cloudflared chart to preserve a healthy quick
tunnel's URL. Quick-tunnel stacks never recover it in that run — the tunnel
stays dormant, so SyncTunnelConfigMap is not called again. serviceoffer-
controller then falls back to defaultBaseURL for the ERC-8004 registration doc
and the OpenAPI `servers` block, and the frontend loses the agent registration
and marketplace URLs.

Applying under a dedicated field manager leaves tunnelURL owned by kubectl and
untouched; a later SyncTunnelConfigMap still overwrites both via
--force-conflicts. Verified on the same cluster.

Claude-Session: https://claude.ai/code/session_015Yed7pSbwnaEUya6ja3j2z
`obol upgrade` re-syncs the same defaults helmfile as `obol stack up` but did
not export OBOL_STACK_VERSION, so obol-frontend.yaml.gotmpl rendered the env
var as "". The podspec then differed from what stack up produced and the
frontend deployment rolled on every upgrade for no reason.

Claude-Session: https://claude.ai/code/session_015Yed7pSbwnaEUya6ja3j2z

@bussyjd bussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving after end-to-end validation on a live k3d cluster.

The tunnelURL prune is real and the fix works. Both binaries were run against the same live stack, and both runs took the same Active quick tunnel detected — skipping cloudflared chart branch — so this is the actual scenario, not an analogue.

pre-fix (cb6f2f24) fixed (7a0f31a9)
ConfigMap after stack up {"obolVersion":"0.14.0-rc1"} {"obolVersion":"0.14.0-rc1","tunnelURL":"https://<quick-tunnel>.trycloudflare.com"}
kubectl manager owns {obolVersion} — shrank {obolVersion, tunnelURL} — intact

The pre-fix run exited 0 with the tunnel still 1/1 ready, and the sell-offer resume at the end of that same run did not restore the field — confirming it is unrecoverable on a quick-tunnel stack without an explicit obol tunnel restart.

The blast radius is worse than the PR body describes. It is not just the ERC-8004 registration doc falling back to a default base URL. With tunnelURL absent, serviceoffer-controller re-rendered the public catalog against the local fallback, and /api/services.json — served over a live public tunnel — began advertising local-only endpoints for every offer:

"endpoint": "http://obol.stack:8080/services/demo-hello"

Every absolute URL in that document was http://obol.stack, unreachable for any remote buyer. It flipped back to tunnel URLs as soon as the field was restored. Worth folding into the PR body so the severity is on record.

Also verified:

  • OBOL_STACK_VERSION is rendered into the obol-frontend podspec by helmfile.
  • The ConfigMap read path works with the env var removed — no RBAC change needed, as claimed.
  • The footer renders the version correctly with obol-stack-front-end#460 deployed.

Unrelated bug found while validating, filed separately as #804: a transient host-port conflict permanently degrades k3d.yaml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants