feat(rollout): release channel domain service (channels and scopes) - #1016
Draft
rl-block wants to merge 1 commit into
Draft
feat(rollout): release channel domain service (channels and scopes)#1016rl-block wants to merge 1 commit into
rl-block wants to merge 1 commit into
Conversation
This was referenced Sep 4, 2026
🔐 Codex Security Review
Review SummaryOverall Risk: HIGH Findings[HIGH] Automated review incomplete
NotesHuman review is required because the bounded automated review was incomplete. Generated by Codex Security Review | |
rl-block
force-pushed
the
rollout/01c-release-channel-domain
branch
from
September 4, 2026 09:39
abd73ab to
fa30a1c
Compare
This was referenced Sep 4, 2026
rl-block
force-pushed
the
rollout/01c-release-channel-domain
branch
from
September 4, 2026 10:10
fa30a1c to
4d90bfe
Compare
rl-block
force-pushed
the
rollout/01c-release-channel-domain
branch
2 times, most recently
from
September 4, 2026 10:36
3a7d96c to
3f03628
Compare
rl-block
force-pushed
the
rollout/01c-release-channel-domain
branch
from
September 4, 2026 12:06
3f03628 to
b3bb4d7
Compare
rl-block
force-pushed
the
rollout/01c-release-channel-domain
branch
from
September 8, 2026 02:32
b3bb4d7 to
0463a93
Compare
rl-block
force-pushed
the
rollout/01c-release-channel-domain
branch
from
September 8, 2026 03:00
0463a93 to
4ae13c6
Compare
Channel CRUD under a per-org advisory lock with overlap rejection, scope preview with truncated model and conflict lists plus totals, and the paged reads the contract defines: channel miners filtered by observed identity, manufacturer/model groups joined to their assignment (checksum, snapshotted target keys, generation, resolved file id and availability, on-target count by provenance, first ten reported versions with the total), and membership conflict relations with winner/loser/excluded-tie resolution. Pair keys are canonical printable ASCII compared with an ASCII-only fold, matching the schema and the files service. Behavior carries the coverage percent and the delegated-control timeout; the delegated method is rejected until its slice lands.
rl-block
force-pushed
the
rollout/01c-release-channel-domain
branch
from
September 8, 2026 03:17
4ae13c6 to
fd1c5e8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reviewable diff: +848/-0 across 2 files (excludes generated, test, and story files).
Summary
Adds the channel half of the
rolloutdomain package: operators can create, edit and delete release channels, preview what a scope covers before saving, and are prevented from putting the same miner in two channels. Membership follows fleet placement, so a miner that moves into a channel's rack is a member on the next read without anyone editing the channel. The rollout engine that acts on assignments is the next PR.Stack. #1014 (merged: API contract) -> #1015 (schema, queries, store) -> #1016 (this PR) -> #1017 (rollout engine) -> #1018 (API wiring) -> #1019 -> #1020 -> #1021 -> #1022 -> #1023. Diff is relative to #1015. Upstream context:
release_channel_member(#1015) is the single source of "who is in which channel" and already resolves overlaps by selector specificity;ResolveReleaseChannelScopeevaluates an unsaved scope and reports the owning channel of any miner another channel already covers;LockReleaseChannelScopesis a per-org, transaction-scoped advisory lock. Out of scope:ApplyFirmware/RollbackFirmware, rollouts and the enforcement loop (#1017); exposing any of this over RPC (#1018).How it works
Creating or editing a channel runs in one transaction: take the org's scope lock, resolve the requested scope with
PreviewScope, fail withFailedPreconditionnaming the offending channels and miner counts if anything is already claimed (the channel being edited is excluded from that check), then insert or update the channel row and replace its target rows. Miner identifiers in the scope are resolved to device ids up front and an unknown identifier is anInvalidArgument. Names are trimmed, must be non-empty, and a unique-violation from the database becomes "a release channel named X already exists". The lock is what makes two operators saving overlapping scopes at the same time serialize instead of both succeeding.Behaviour validation (
Behavior.validate) fills defaults (all_at_once,least_efficient_first), rejects unknown methods and orders, requires a batch size for batched and a pilot size for pilot-then-continue, range-checks thresholds (hashrate drop 0–100 %, the rest non-negative), and normalizes away knobs the chosen method cannot use (e.g. an all-at-once channel drops batch size, review, auto-continue and thresholds; a pilot channel always reviews after the pilot). What is stored is therefore exactly what the engine will read.Reading channels loads the org's targets, members, assignments and active rollouts in four queries and assembles
Channelviews grouped by model: eachModelGroupis a summary — the assigned firmware (if any),MinerCount,OnTargetCount(members reporting the assigned version), the sortedReportedVersions, and the id of the active rollout for that model. Members themselves are read throughListChannelMiners, which returns one page (default 100, max 1000) of a channel's miners ordered by identifier, optionally one model, each with its reported version and aConflictedflag; the opaque cursor is the last row's(identifier, id).Previewing a scope returns miners per model and a list of
ScopeConflict{ChannelID, ChannelName, MinerCount}; the same function backs both the UI preview and the write-time rejection so they cannot disagree.Diagrams
flowchart TD Op["CreateChannel / UpdateChannel (spec)"] --> V["spec.validate: trim name, normalize scope, Behavior.validate"] V --> Tx["RunInTx"] Tx --> L["LockReleaseChannelScopes (org advisory lock)"] L --> P["PreviewScope via ResolveReleaseChannelScope (exclude self)"] P -->|conflicts| E["FailedPrecondition: scope overlaps release channel X (n miners)"] P -->|clean| W["insert / update release_channel"] W --> T["replaceTargets: resolve identifiers to device ids, rewrite release_channel_target"] T --> G["GetChannel: targets + members + firmware + active rollouts, grouped by model"]Areas of the code involved
server/internal/domain/rollout/service.go(new)Serviceand its dependencies (store, transactor, command dispatcher, firmware files, activity log); view typesChannel,ModelGroup(summary counts),ChannelMiner,ScopePreview,ScopeConflict;CreateChannel,UpdateChannel,DeleteChannel,ListChannels,GetChannel,ListChannelMiners,PreviewScope,rejectOverlap,replaceTargets,buildChannels; shared paging helpers (clampPageSize,encodeCursor/decodeCursor,DefaultPageSize= 100,MaxPageSize= 1000)excludeChannelIDis threaded through on updateserver/internal/domain/rollout/behavior.go(new)Scope(normalize,IsEmpty, target rows),Thresholds,BehaviorwithvalidateandgatesAfterBatch,behaviorFromChannel, null helpersserver/internal/domain/rollout/channel_test.go(new)Conflictedflag; duplicate name; delete; behaviour validation and normalizationserver/internal/domain/rollout/fixture_test.go(new)dbtest, fakes for dispatcher / firmware files / activity, placement helpers (addSite,addRack,placeInSet,placeAtSite, …)Key technical decisions & trade-offs
Conflictedon the winning (more specific) channel; auto-resolving by editing scopes behind the operator's back was rejected.PreviewScopeis the single overlap oracle for both the UI and the write path, so what the operator sees before saving is what the server enforces.Servicealready takes the command dispatcher and firmware-files dependencies its constructor will need in feat(rollout): firmware rollout engine #1017, so wiring (feat(rollout): expose RolloutService and run the enforcement loop #1018) does not change when the engine lands.buildChannelsstill walks the org's members once to compute counts, but the API never embeds a miner list in a channel, so response size is bounded by the number of models, not miners.Contract alignment (after the #1014 merge)
#1014 merged on 2026-09-08 as
74454ed9. This stack was cut against the contract as it stood on 2026-09-04 (4256f05f); the contract then absorbed thirty Codex review rounds before merging. On 2026-09-08 the branch was rebased ontomainso it carries only its own commit. It compiles and its tests and lint pass on the rebased head.This PR implements: Create/update/delete with advisory-locked overlap rejection, scope preview, model-group summaries and paged channel miners.
Contract surface added after the stack was cut, not in this PR:
(manufacturer, model)with ASCII-fold uniqueness (RequiredManufacturerModelTargetKeyin the contract tests).ListReleaseChannelModelGroupsis its own paged RPC carryingreported_version_count,on_target_countby provenance,assignment_generation,firmware_checksumandfirmware_available.model_count/conflict_countwith 100-item truncation.These gaps are tracked in the stack status note and are the subject of the reconciliation plan for the next revision of this stack.
Testing & validation
DB_PASSWORD=fleet go test ./internal/domain/rollout/: 2 integration tests against a real Postgres/TimescaleDB (dbtesttemplate clone), now also covering model-group summaries andListChannelMinerspaging, model filter, conflict flag, bad cursor and unknown channel; plus the contract validation tests from feat(rollout): RolloutService API contract for firmware release channels #1014.golangci-lint run ./internal/domain/rollout/...clean.