Skip to content

feat: server-advertised updates from a configurable release repository - #19

Merged
immakermatty merged 4 commits into
mainfrom
claude/DEV-6624-t3-update-channel
Sep 26, 2026
Merged

immakermatty merged 4 commits into
mainfrom
claude/DEV-6624-t3-update-channel

Conversation

@immakermatty

@immakermatty immakermatty commented Sep 25, 2026 •

Copy link
Copy Markdown

Proč

Operátor, který používá web verzi T3 Code servírovanou headless serverem (Linux VM nebo Mac Mašina), dnes nikdy neuvidí nabídku aktualizace serveru. Banner „Server update available“ se ukáže jen tehdy, když je klient novější než server, a prohlížeč servírovaný tím samým serverem má vždy stejnou verzi. Když už se banner objeví (třeba z upstream desktop klienta), cílová verze je verze klienta, kterou náš kanál nemusí vůbec mít. Upstream cesta „zkopíruj npx t3@<verze> a spusť ji v terminálu“ je pro operátora nešikovná.

Matěj 2026-09-25 rozhodl: operátor má vidět standardní banner s tlačítkem Update, když na nakonfigurovaném kanálu vyjde novější release, a kliknutí spustí už existující upstream self-update (launcher stáhne t3-<ver>-<platform>.tar.gz + SHA256SUMS, ověří, vyzkouší a přepne s rollbackem).

Co PR zavede

První dva commity jsou napsané obecně (bez Lazurio názvů v kódu), aby šly nabídnout upstreamu beze změny:

  1. feat(server): install and update from a configurable release repository

    • Nová proměnná T3CODE_RELEASE_REPOSITORY (owner/name, výchozí pingdotgg/t3code). Používá ji index vydání, t3 update, boot service (t3 service install), vzdálený self-update i install.sh/install.ps1. Stahování jde z GitHub Releases tohoto repa, pokud T3CODE_RELEASE_BASE_URL neurčí mirror.
    • Index vydání vybírá nejvyšší verzi kanálu podle SemVer včetně prerelease (0.0.42-lazurio.10 > .9), ne první v pořadí publikace. Čte všechny stránky indexu až po poslední (max. 10 požadavků) a porovnává verze napříč nimi. Drafty přeskakuje a tagy, které nejsou přesný SemVer (např. v0.0.43-lazurio.01), zahazuje stejným pravidlem jako launcher (isExactServiceVersion).
    • Install skripty klasifikují tagy stejně jako runtime (cliReleaseChannelOf): vlastní vlak mají jen nightly/preview hned za major.minor.patch, všechno ostatní (i -lazurio.N nebo -acme-nightly.…) je stable; stejné pravidlo platí i pro varování o preview. Bez toho by install.sh z našeho repa nenašel žádný stable release.
    • Když požadovaná verze v repu není (404 na SHA256SUMS), server i CLI odpoví jasně: t3@X is not published in Lazurio/t3code releases. (případně název mirroru) místo obecného „Could not prepare t3@X“.
    • Resolver indexu vydání se přesunul z t3 update CLI do sdíleného apps/server/src/cloud/releaseIndex.ts.
  2. feat: offer a managed server's own newer release as its update target

    • Server běžící pod boot-service launcherem (serverSelfUpdate: boot-service) kontroluje při startu a pak každých 6 hodin nejnovější release na svém vlastním kanálu v nakonfigurovaném repu. Při chybě zkouší znovu s exponenciálním backoffem (1 min → max 6 h) a drží poslední odpověď.
    • Nekontroluje, když server není managed, když běží preview build nebo když operátor nastaví T3CODE_UPDATE_CHECK_ENABLED=false. Proměnnou čte až po této kontrole, takže chybná hodnota neshodí server, který kontrolovat nemá.
    • Výsledek vystaví jako volitelné pole deskriptoru prostředí availableServerUpdate: { version }, jen když je verze striktně vyšší než serverVersion (stejné SemVer pravidlo, jaké vynucuje launcher). Schéma je zpětně kompatibilní (optionalKey, starší klienti pole ignorují).
    • Web: když server pole posílá a je boot-service managed, stávající banner / řádek v Settings → Connections / hromadný update nabídnou právě tuto verzi jako targetVersion. Stejné komponenty, stejný progress UI, stejná volba pokračování vláken. Funguje i v prohlížeči servírovaném tím samým serverem. Pokud platí zároveň client-skew, vyhrává verze od serveru (server ví, z jakého kanálu umí instalovat; upstream desktop klient novější než náš kanál tak nevyrobí cíl, který skončí 404). Jinak se client-skew logika nemění.
    • Klíč pro zavření banneru je teď podle nabízené cílové verze, takže zavření .2 neschová .3. Pro client-skew je klíč identický jako dřív.

Záměrně se nemění: launcher, protokol self-updatu, preflight, rollback, server.updateServer kontrakt, mobilní aplikace (nemá UI pro update serveru), desktop-managed servery, chování klienta pro servery bez nového pole. Nepřidává se nový stream event: nová verze se klientovi dostane v dalším snapshotu konfigurace (připojení, reconnect, reload stránky), ne okamžitě do již otevřeného tabu.

Rollout pro Lazurio

  • Mašina musí běžet pod boot-service launcherem a mít v prostředí služby T3CODE_RELEASE_REPOSITORY=Lazurio/t3code (unit z t3 service install sám nastavuje jen T3CODE_HOME; launcher předává prostředí dítěti). To je práce pro Machines.
  • Vydání musí nést tagy vX.Y.Z-lazurio.N s archivy a SHA256SUMS (paralelní PR release: publish CLI archives as the Lazurio update channel #18). Nepoužívat GitHub draft, index drafty přeskakuje.
  • SemVer: 0.0.42-lazurio.1 < 0.0.42. Server hlásící 0.0.42 proto první přechod na náš kanál nenabídne (a launcher by ho stejně odmítl); první převod přes pin v Machines nebo t3 update 0.0.42-lazurio.1 --allow-downgrade.

Fork CI guard (třetí commit)

Fork CI (lazurio-fork-ci.yml) dosud odmítal jakýkoli overlay pod apps/(web|mobile|desktop) a packages/. Tahle funkce z principu mění web banner, kontrakt deskriptoru a sdílený index vydání, proto na rozhodnutí delegujícího agenta (Matějovo rozhodnutí o tlačítku Update má přednost před starým pravidlem) commit ci: allow the reviewed in-app update files in the fork overlay guard:

  • Guard zůstává fail-closed. Místo „cokoli pod těmi kořeny padá“ má explicitní allowlist přesně souborů tohoto PR, každá skupina s jednořádkovým důvodem. Jakákoli jiná cesta pod těmi kořeny padá a chybová hláška ji vypíše.
  • Allowlist:
    • In-app update from the configured release channel; proposed upstream: apps/web/src/components/ChatView.tsx, apps/web/src/components/chat/useAutoBalanceUpdateBanner.tsx, apps/web/src/components/settings/ConnectionsSettings.tsx, apps/web/src/versionSkew.test.ts, apps/web/src/versionSkew.ts
    • Optional availableServerUpdate descriptor field; proposed upstream: packages/contracts/src/environment.ts
    • Configurable release repository and semver release index; proposed upstream: packages/shared/src/cliRelease.test.ts, packages/shared/src/cliRelease.ts
  • scripts/lazurio-release-contract.test.mjs má nový samostatný test (existující testy nemění, aby rebase s PR release: publish CLI archives as the Lazurio update channel #18 byl triviální): guard dál filtruje ty kořeny a failuje, allowlist začíná důvodem a každá položka je přesný existující soubor pod hlídaným kořenem, bez globů, adresářů a duplicit.
  • Guard ověřen lokálně i negativně: na HEAD projde, s extra změnou v packages/shared/src/semver.ts spadne a soubor vypíše.

docs/operations/lazurio-fork-release.md (tabulka overlay commitů a věta „Nothing in the overlay touches clients, shared packages, or wire contracts“) aktualizuje přepis runbooku v PR #18; tady se záměrně nemění, aby nevznikl konflikt.

Review (Pablo, 3b8ca84) a opravy

Čtvrtý commit fix: pick installable releases across the whole index and one train rule řeší všechny čtyři nálezy, každý s regresním testem, který na předchozím HEADu padá:

  1. [P1] Resolver bral první stránku s trefou kanálu → teď čte všechny stránky (max. 10) a porovnává napříč. Test: .1 + 99 nightlies na stránce 1, .2 na stránce 2 → .2.
  2. [P2] Nepřesné SemVer tagy (v0.0.43-lazurio.01) → zahozeny přes isExactServiceVersion před výběrem. Test: neplatný nejnovější tag vedle platného .2.
  3. [P2] Install skripty neukotvovaly nightly/preview suffix → pravidlo runtime v obou skriptech. Nový scripts/install-scripts.test.ts spouští skutečný install.sh proti falešnému curl a ověřuje pravidlo install.ps1 proti cliReleaseChannelOf, včetně v1.2.3-acme-nightly.20260901.1 (stable všude).
  4. [P2] T3CODE_UPDATE_CHECK_ENABLED se četl před kontrolou způsobilosti → pořadí prohozeno. Test: maybe u unmanaged serveru i preview buildu.

Greptile navíc hlásil „Fork updates become unavailable“ (versionSkew fallback na verzi klienta). Záměrně beze změny: fallback je upstream chování a server teď neznámou verzi jasně odmítne (not published in <repo> releases); nový signál kvůli nálezu bota nepřidáváme. Vlákno je zodpovězené a vyřešené, stejně jako ostatní tři.

Ověření

Lokálně na HEAD (Node 26 / pnpm 11.10.0, pnpm install --frozen-lockfile):

  • vp fmt --check – OK
  • typecheck: vp run --filter t3 typecheck, vp run --filter @t3tools/web typecheck, packages/shared, packages/contracts, scripts – OK
  • vp lint --report-unused-disable-directives – 0 chyb; knip --exports pro server/shared/web/contracts – čisté
  • apps/server: vp test run src/cli/config.test.ts src/auth src/environment/ServerEnvironmentLabel.test.ts src/environment/ServerEnvironment.test.ts src/cloud src/cli/update.test.ts src/cli/service.test.ts – 28 souborů, 280 testů OK; src/server.test.ts – 189 OK
  • packages/shared cliRelease.test.ts + semver.test.ts – 21 OK; packages/contracts environment.test.ts – 5 OK
  • apps/web versionSkew.test.ts + ServerUpdateAction.test.tsx – 35 OK
  • scripts install-scripts.test.ts – 4 OK (install.sh reálně; install.ps1 jen jeho regex, pwsh není k dispozici)
  • node --test scripts/lazurio-release-contract.test.mjs – 7/7 OK

Neověřeno end-to-end na skutečné Mašině (chybí vydání na kanálu a env ve službě).

Rizika

  • GitHub API bez tokenu má limit 60 požadavků/h na IP; kontrola jednou za 6 h na server čte nejvýš 10 stránek (u našeho repa 1), takže je hluboko pod limitem i za NATem.
  • Nová verze se do již otevřeného tabu dostane až s dalším snapshotem konfigurace (reconnect/reload). Push přes stream event by vyžadoval nový typ eventu s capability gatem – případný follow-up.
  • Změna pravidla stable kanálu v install skriptech se týká i upstreamu (tagy typu -rc.1 by byly stable); srovnává je s tím, co už dnes dělá t3 update.

Upstream proposal

Headless servers offer their own updates, from a configurable release repository.

Today a browser served by t3 serve never sees a server update: the notice only fires when the client is newer than the server, and the target it installs is always the client's version. Operators of headless hosts (a Linux VM, a spare Mac) are left copying npx t3@<version> into a terminal. Forks that publish their own archives also cannot point the runtime at their releases without a mirror.

This change:

  1. Adds T3CODE_RELEASE_REPOSITORY (default pingdotgg/t3code), honored by the release index, t3 update, the boot service, remote self-update, and the install scripts. Downloads follow it unless T3CODE_RELEASE_BASE_URL sets a mirror.
  2. Picks the newest release on a channel by semver precedence (including prereleases) across every index page, skipping tags the launcher would refuse as not exact SemVer, instead of the first match in publish order. The install scripts classify trains by the same rule as the runtime.
  3. Lets a boot-service-managed server check its own channel at startup and every six hours (backoff on failure; skipped when unmanaged, on preview, or with T3CODE_UPDATE_CHECK_ENABLED=false) and advertise a strictly newer release as an optional availableServerUpdate: { version } field on the environment descriptor.
  4. Has clients offer that version through the existing update notice, Connections settings, and bulk update, preferring it over client-version skew for boot-service servers, since the server knows which train it can actually install from.
  5. Reports a missing release plainly (t3@X is not published in <repository> releases.) instead of a generic prepare failure.

No protocol, launcher, or rollback changes; the descriptor field is optional, so old clients and old servers are unaffected.

🤖 Generated with Claude Code

immakermatty and others added 3 commits September 26, 2026 00:10
T3CODE_RELEASE_REPOSITORY (owner/name, default pingdotgg/t3code) now selects
whose GitHub Releases the release index, `t3 update`, the boot service, remote
self-update and the install scripts use. Downloads follow the repository
unless T3CODE_RELEASE_BASE_URL names a mirror, so a fork can publish its own
archives without a mirror.

The release index picks the highest version on a channel by semver
precedence instead of the first match in publish order, so a later patch for
an older line no longer wins. The install scripts classify tags with the same
rule the runtime uses: only nightly and preview prereleases form their own
trains, every other version is stable.

Asking for a version whose SHA256SUMS is missing now fails with
"t3@<version> is not published in <repository> releases." (or the mirror)
from both `t3 update` and server.updateServer, instead of a generic
"Could not prepare" error. The release-index lookup moves from the update
CLI into apps/server/src/cloud/releaseIndex.ts so the server can reuse it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A server run by the boot-service launcher never showed an update in the web
app it serves, because the notice only fired when the client was newer than
the server, and the target it sent was always the client's version, which
may not exist on the release train the server installs from.

The server now checks its own channel in its release repository at startup
and every six hours (backing off on failure; never when unmanaged, on
preview builds, or with T3CODE_UPDATE_CHECK_ENABLED=false) and advertises a
strictly newer release as the optional `availableServerUpdate` descriptor
field. Clients offer that version through the existing update notice,
Settings -> Connections row and bulk update, with the same progress UI and
thread continuation. When both apply, the server's advertised release wins
over client-version skew. Dismissals are keyed by the offered target, so
dismissing one release does not hide the next; the key is unchanged for
client skew.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fork CI rejected every overlay change under apps/web, apps/mobile,
apps/desktop and packages. The in-app update from the configured release
channel (decided by the Organization Admin on 2026-09-25) has to change the
web update notice, the environment descriptor contract and the shared
release index, so the guard now allows exactly those files, each group with
its reason. It stays fail-closed: any other path under those roots fails and
is listed in the error.

The release contract test asserts the guard still filters those roots, that
the allowlist opens with a reason, and that every entry is an exact existing
file under a guarded root (no globs, directories or duplicates).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@immakermatty
immakermatty marked this pull request as ready for review September 25, 2026 22:21
Comment thread apps/server/src/cloud/releaseIndex.ts Outdated
Comment thread apps/web/src/versionSkew.ts
Comment thread apps/server/src/cloud/releaseIndex.ts Outdated
Comment thread scripts/install.sh Outdated

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review of exact head 3b8ca84 (base 7b61df2): changes requested.

  1. [P1] apps/server/src/cloud/releaseIndex.ts:76-77 — The resolver returns the first channel hit on page 1 instead of the highest SemVer over the searched pages. With 0.0.42-lazurio.1 and 99 nightlies on page 1, and 0.0.42-lazurio.2 on page 2, it returns .1 and never fetches page 2; a server already on .1 advertises nothing. Accumulate the best candidate over the bounded pages (or prove an ordering-based early stop) and test this case.

  2. [P2] packages/shared/src/cliRelease.ts:136-138, apps/server/src/cloud/releaseIndex.ts:104 — The tag regex accepts non-exact SemVer such as v0.0.43-lazurio.01; the server advertises it as newer than 0.0.42-lazurio.2, but isExactServiceVersion rejects it at the self-update/launcher boundary. Exclude invalid tags before choosing a candidate and add a regression for an invalid newest tag alongside a valid release.

  3. [P2] scripts/install.sh:86-90 and scripts/install.ps1:53-56 — The nightly/preview suffix regex is not anchored immediately after major.minor.patch. Both installers classify v1.2.3-acme-nightly.20260901.1 as nightly, while cliReleaseChannelOf classifies it as stable. Align both installer filters with the runtime channel rule and test the cross-surface case.

  4. [P2] apps/server/src/cloud/releaseIndex.ts:119-123 — Config.boolean is parsed before checking managed/preview status. An inherited malformed T3CODE_UPDATE_CHECK_ENABLED value can fail construction of an unmanaged server that should not perform any check. Apply the eligibility gate before reading this flag (or otherwise avoid making an irrelevant setting fatal).

Focused existing checks passed (64 server/shared/web tests, 19 UI/contract tests, and 7 release-contract tests); two extra disposable regression tests for findings 1 and 2 failed on this head with the actual wrong values. The update action remains click-triggered and the archive remains SHA256-verified. The unchanged client-skew fallback is deliberate per the stated scope, so I am not counting that existing behavior as a new defect.

Review of 3b8ca84 found four gaps, each now covered by a regression test:

- The release resolver returned the best release of the first page with a
  channel hit. The index is ordered by publish time, so a higher release on
  a later page was missed. It now reads every page up to the last one (at
  most 10 requests) and compares versions across all of them.
- Tags that are not exact SemVer, such as v0.0.43-acme.01, could be chosen
  and advertised although the launcher refuses them. They are dropped with
  the launcher's own isExactServiceVersion before choosing.
- install.sh and install.ps1 matched a nightly or preview suffix anywhere in
  the tag, so v1.2.3-acme-nightly.20260901.1 was nightly to the installers
  but stable to the runtime. Both now use the runtime rule (the train
  identifier directly after major.minor.patch), also for the preview
  warning. scripts/install-scripts.test.ts runs install.sh against a fake
  curl and checks install.ps1's rule against cliReleaseChannelOf.
- T3CODE_UPDATE_CHECK_ENABLED was parsed before the managed/preview gate, so
  a malformed value could fail an unmanaged server that never checks. The
  gate now runs first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review of exact head a5a1725 (base 7b61df2): the four blocking findings from review 5323299236 are addressed. The resolver scans bounded pages and chooses the highest valid exact SemVer; the install.sh and install.ps1 train filters match the runtime (including fork-suffixed stable tags); and unmanaged/preview servers skip malformed update-check configuration. Focused server/shared/install/release-contract tests passed (10+21+4+7), scoped typechecks and formatting passed; the real PowerShell installer selected the expected stable/nightly/preview tags with mocked downloads. CI is green on this head and all four conversations are resolved. The unchanged upstream client-version-skew fallback is not a new blocker. No real-machine release/update smoke was performed.

@immakermatty
immakermatty merged commit 7cbd4ad into main Sep 26, 2026
4 checks passed
immakermatty added a commit that referenced this pull request Sep 26, 2026
The update-channel change (PR #19) intentionally edits listed upstream
client and shared files. The runbook now names that overlay item and
tells the release owner to re-check the allowed_upstream_changes list on
every upstream rebase.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
immakermatty added a commit that referenced this pull request Sep 26, 2026
The update-channel change (PR #19) intentionally edits listed upstream
client and shared files. The runbook now names that overlay item and
tells the release owner to re-check the allowed_upstream_changes list on
every upstream rebase.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@immakermatty
immakermatty deleted the claude/DEV-6624-t3-update-channel branch September 26, 2026 11:46
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