feat(control-plane): pin per-tenant image versions for fleet rollout and rollback - #8056
Conversation
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-22 17:27:15 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
Summary
Closes #4898.
Implements the fleet rollout mechanism exactly as the issue's ratified decision (2026-07-21) specifies: a
pinned_versionfield on the control-plane's tenant record, rollout/rollback as an explicit-list update of that field, and each tenant's container reading its own pinned version at (re)start instead of a shared:latest.Tenant.pinnedVersion(control-plane/src/tenant-provisioning-driver.ts): product-agnostic — the identical field for ORB and AMS tenants, per feat(control-plane): provisionTenant()/deprovisionTenant() orchestration behind an injectable driver interface (fake driver only) #7524's product-agnostic requirement. Absent/null= unpinned (release-channel default, exactly the pre-Build a fleet rollout mechanism #4898 behavior). It persists for free inside the registry record'stenantJSON (tenant-registry.tsneeded zero interface changes; pre-existing KV records read back unpinned).control-plane/src/container-driver.ts): a Cloudflare Container binding is fixed to one image at the wrangler.jsonc level (that file's own documented constraint), so per-tenant versioning can't swap the image reference binding-side — instead a pinned tenant's version rides intostub.start()'s existingenvVarsoption asLOOPOVER_PINNED_VERSION, which the container's own entrypoint reads to resolve its versioned artifact — the ratified "each tenant's container reads its own pinned version at (re)start" mechanism. An unpinned tenant gets the exact pre-Build a fleet rollout mechanism #4898start()call (no options object at all), pinned to a byte-identical-behavior test, so every existing tenant is untouched until a rollout pins it.POST /v1/tenants/rollout(control-plane/src/http-app.ts):{ names: [...], pinnedVersion: "vX" | null }— an explicit tenant-name list (no percentage/canary selector, per the issue's "don't invent one speculatively" boundary),null= explicit unpin/revert. All-or-nothing: the whole list is validated (unknown name → 404, torn-down tenant → 409, malformed body → 400) before any record is touched, so a typo'd name can never leave a fleet half rolled out. Sits behind the same Bearer wall as every other/v1/tenants/*route.GET /v1/tenantssurfaces each tenant's pin for admin visibility.Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Closes #123) — a linked open issue is required for every contributor PR.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally;codecov/patchrequires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderateIf any required check was skipped, explain why:
control-plane/— a standalone npm package with its own build, test runner, and coverage pipeline, none of which the root commands above touch (control-plane/**is not in the root vitest config; its CI job runscontrol-plane:test+control-plane:coverage). I ran the checks that actually exercise this package:npm --prefix control-plane test(tsc build + fullnode:testsuite: 107/107 pass, including 13 new tests for this change),npm run control-plane:coverage(the same c8 harvest CI uploads to Codecov), andcontrol-plane'scf:typecheck(the Worker tsconfig).control-plane/coverage/lcov.infoDA/BRDArecords withgit diff -U0againstupstream/main: 100% of changed lines and 100% of changed branches in all four touched source files (thecodecov/patchgate applies tocontrol-plane/via its dedicated flag).npm audit: this PR adds zero dependencies (root andcontrol-plane/package.json/lockfiles untouched).Safety
UI Evidencesection below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. SVG screenshots are not used as review evidence. Review-only screenshots or recordings are not committed to the repository.The two unchecked Safety boxes are N/A: no UI change of any kind (control-plane backend only).
Auth negative path: the new route is under the existing
/v1/tenants/*Bearer middleware, and a test pins that an unauthenticatedPOST /v1/tenants/rolloutgets 401. The rollout responses reusesafeRecord— a tenant's database connection details still never appear on the wire (this endpoint never touches them at all).UI Evidence
N/A — no UI change (control-plane backend only).
Notes
updatedAtchurn); the rollback test re-pins one tenant and asserts the other's pin is unchanged; the unpin (null) path reverts one tenant while the other stays pinned.POST /v1/tenants) starts it unpinned, consistent with the create route's existing "fresh provision, not a resurrection" doc comment.