Update Channels + Rollback / Pin Specification¶
- Authors
- Matt Cockayne, Claude Opus 4.8 (AI drafting assistant)
- Date
- 21 June 2026
- Status
- DRAFT
Summary¶
GTB's self-updater (pkg/setup.SelfUpdater) discovers the single latest
release via release.Provider.GetLatestRelease and, when a --version is
given, an exact tag via GetReleaseByTag. There is no notion of a release
channel: a tool author cannot publish a beta line alongside stable, and a user
cannot opt their installation into "give me betas" or "only stable". Equally,
while a user can target an exact tag with update --version vX.Y.Z, there is
no first-class, durable concept of a pin (stay on this version, do not drift)
or a rollback (deliberately move backwards to a known-good prior release) β
and the updater's IsLatestVersion logic actively treats a lower target as "your
tardis travelled too far into the future", i.e. it is built around the assumption
that updates only ever go forward.
This spec adds, item A6 on the roadmap:
- Update channels β
stable|beta|canary, a tool-author opt-in, resolved from the semver prerelease label of each release tag (e.g.v1.2.0= stable,v1.2.0-beta.1= beta,v1.2.0-canary.3= canary). No new release infrastructure is required: channels are a view over the existing tag stream, computed locally. - Pin β durably fix the installation to an exact version so neither the
background check nor an unqualified
updatemoves it. - Rollback β deliberately and safely move backwards to a prior release, re-running the full checksum + signature verification pipeline against the target, so a downgrade is exactly as trustworthy as an upgrade.
It is purely additive and backwards compatible: a tool that sets no channel behaves precisely as today (stable-only, forward-only).
Goals & Non-Goals¶
Goals¶
- A tool author can opt into publishing/consuming channels via a
Tool.UpdateChannelbaseline, overridable at runtime by anupdate.channelconfig key (mirroring the existingupdate.policy/update.check_intervaltwo-tier resolution from the ForcedUpdate spec). - Channel selection is computed locally from the semver prerelease label of
release tags β no dependency on a provider "prerelease" boolean (the
release.Releaseinterface does not expose one; see Design Decision 1). - A user can pin (
update --pin [version]) and unpin (update --unpin), with the pin persisted next to the existing update markers in the tool config dir and honoured by the background check. - A user can roll back (
update --rollback [version]) to a prior release with the same checksum + signature verification the forward path enforces, and with a clear, explicit confirmation that this is a downgrade. - The background update check (
pkg/cmd/root) respects the channel and the pin: a pinned tool never nags about a newer release on its channel. - Backwards compatible: no channel + no pin = today's stable, forward-only behaviour, byte-for-byte.
Non-Goals¶
- No new release artefacts or publishing pipeline. Channels are derived from existing tags; GoReleaser/releaser-pleaser output is unchanged. (How a tool author chooses to tag betas is their concern; we document the convention.)
- No staged/percentage rollout, A/B cohorts, or remote channel assignment. A channel is a local selection, not a server-driven feature-flag system. (Noted under Future Considerations.)
- No relaxation of verification for downgrades. Rollback is not a bypass β
it is a direction.
require_checksum/require_signatureapply identically. - No automatic rollback-on-failure of a forward update (the in-place replace already restores on failure; an automatic "revert to previous binary" cache is a separate, larger feature β Future Considerations).
- Not a config-profile / environment-overlay mechanism. See the explicit non-conflict note below.
Relationship to the Rejected --profile Feature (non-conflict)¶
The decision log
(docs/development/feature-decisions.md Β§
"Environment Profiles (dev/staging/prod)") rejected a --profile flag that
would select environment-specific config overlays (config.dev.yaml), on the
grounds that GTB should not opinionate on how users compose configuration β the
existing multi---config mechanism already covers it.
Update channels are categorically distinct and do not reopen that decision:
Rejected --profile |
This spec's channels |
|---|---|
| Selects which config file(s) to merge | Selects which release tag to install |
| A configuration-composition concern | A release-selection concern |
Overlaps existing --config a --config b |
No existing mechanism (the updater only ever sees one "latest") |
| Imposes an opinion on deployment-config layout | Imposes nothing on config; reads one scalar key |
A channel never alters the merged configuration, never selects a config source,
and never changes runtime behaviour beyond which binary version the self-updater
considers a candidate. It is the release analogue of "track" in apt/brew
(stable/edge), not an environment overlay. The single scalar update.channel
key flows through the same existing config precedence chain as every other
key β it does not introduce a parallel overlay system. Confirmed: no conflict
with the --profile rejection. (This spec adds a corresponding entry to the
decision log's "Accepted, with scope" section on approval, to record the
distinction permanently.)
Background β anchor code¶
| Concern | Location | Today |
|---|---|---|
| Updater + options | pkg/setup/update.go SelfUpdater, UpdaterOption, WithReleaseProvider |
Resolves one provider; version field pins an exact tag for a single run |
| Latest/by-tag discovery | pkg/setup/update.go GetLatestRelease, GetLatestVersionString |
GetLatestRelease(owner,repo) or GetReleaseByTag(owner,repo,tag) |
| Forward-only assumption | pkg/setup/update.go IsLatestVersion |
A lower latest β "tardis travelled too far into the future" |
| Release abstraction | pkg/vcs/release/provider.go Provider, Release, ReleaseAsset, ChecksumProvider, SignatureProvider |
Release exposes GetTagName/GetDraft; no prerelease flag |
| List discovery | Provider.ListReleases(ctx,owner,repo,limit) |
Used today only for changelog notes (filterReleaseNotes) |
| Source config | pkg/vcs/release/source_config.go ReleaseSourceConfig, props.ReleaseSource |
Type/Host/Owner/Repo/Private/Params |
| Update command | pkg/cmd/update/update.go NewCmdUpdate |
Flags --force, --version, --from-file (versionβfrom-file) |
| Semver | pkg/version/version.go CompareVersions, FormatVersionString (wraps golang.org/x/mod/semver) |
Compare + prefix normalise |
| Policy/interval resolution | pkg/props/update_policy.go ResolveUpdatePolicy; pkg/setup ResolveCheckInterval |
Two-tier: config key over Tool.* baseline over framework default |
| Background check | pkg/cmd/root/root.go checkForUpdates, shouldSkipUpdateCheck, warnIfBehindCached |
Reads update.policy/update.check_interval; caches latest in last_checked marker body |
Two structural facts shape the design:
release.Releasehas no prerelease boolean. It exposesGetTagName()andGetDraft()only. Channel discrimination therefore must come from the semver prerelease label of the tag, viagolang.org/x/mod/semver.Prereleaseβ which is correct, provider-agnostic, and already the dependencypkg/versionwraps. (GitHub does surface a prerelease flag on its API type, but GitLab / Gitea / Direct do not uniformly, and threading a new method through theProviderinterface + every backend + thereleasetestdouble is a far larger change for no extra signal β the tag label is authoritative.)- The updater is built forward-only.
GetLatestReleasereturns the single newest release;IsLatestVersiontreats "latest < current" as an anomaly. Channels require enumerating candidates (ListReleases) and selecting the newest on the chosen channel; rollback requires explicitly permitting "target < current".
Design¶
Channel model and semver mapping¶
A channel is one of three values, defined in pkg/props alongside
UpdatePolicy (it is a tool-author baseline, set the same way):
// pkg/props/update_channel.go
type UpdateChannel string
const (
ChannelStable UpdateChannel = "stable" // releases with NO prerelease label
ChannelBeta UpdateChannel = "beta" // -beta.* (and, by inclusion, stable)
ChannelCanary UpdateChannel = "canary" // -canary.* (and beta, and stable)
)
Mapping a tag β channel uses semver.Prerelease(tag) (returns e.g. -beta.1,
or "" for a final release):
| Tag | semver.Prerelease |
Native channel |
|---|---|---|
v1.2.0 |
"" |
stable |
v1.2.0-beta.1 |
-beta.1 |
beta |
v1.2.0-canary.3 |
-canary.3 |
canary |
v1.2.0-rc.1 |
-rc.1 |
(see OQ2 β treated as beta by default) |
Channel inclusion is a superset ladder (Decision below): selecting canary
considers canary and beta and stable candidates; beta considers beta +
stable; stable considers only stable. This matches user intuition ("I want the
bleeding edge" implies "β¦but a newer stable is still an upgrade") and means a
channel never strands a user on an older prerelease when a newer stable exists.
Selection is: of all releases whose channel β€ the selected channel in the ladder,
pick the highest by semver.Compare (which already orders 1.2.0-beta.1 <
1.2.0).
A new helper centralises this, mirroring ResolveUpdatePolicy:
// pkg/props/update_channel.go
func (c UpdateChannel) Normalize() UpdateChannel // "" β ChannelStable, lower/trim
func (c UpdateChannel) Valid() bool
// Includes reports whether a release of channel `relCh` is a candidate for a
// user on channel c (the superset ladder).
func (c UpdateChannel) Includes(relCh UpdateChannel) bool
// ChannelOf maps a (normalised) version tag to its native channel.
func ChannelOf(tag string) UpdateChannel
// ResolveUpdateChannel: config value over Tool baseline over ChannelStable.
func ResolveUpdateChannel(toolDefault UpdateChannel, configValue string) UpdateChannel
Selecting the latest release on a channel¶
The updater gains a channel-aware discovery path. Rather than add a method to the
Provider interface, channel filtering is done client-side in pkg/setup
over Provider.ListReleases results β keeping every existing backend and the
releasetest double untouched:
// pkg/setup/update.go (new, on *SelfUpdater)
// GetLatestReleaseForChannel returns the highest-precedence release whose
// native channel is included by ch (per UpdateChannel.Includes), skipping
// drafts. Falls back to GetLatestRelease when ch == ChannelStable AND no
// non-stable tags exist, so the common path is unchanged.
func (s *SelfUpdater) GetLatestReleaseForChannel(ctx, ch props.UpdateChannel) (release.Release, error)
Implementation: ListReleases(ctx, owner, repo, releasesPerPage) (the constant
already exists, =100), drop GetDraft()==true, map each GetTagName() via
props.ChannelOf, keep those ch.Includes(...), and return the
semver.Compare-max. releasesPerPage is the existing single-page bound; paging
beyond 100 releases to find an older channel candidate is a documented limitation
(OQ4).
SelfUpdater gets a channel props.UpdateChannel field, resolved in
NewUpdater from props.Config.GetString("update.channel") over
props.Tool.UpdateChannel (via ResolveUpdateChannel), exactly as policy is
resolved elsewhere. GetLatestRelease / GetLatestVersionString /
IsLatestVersion consult the channel-aware path when s.channel != ChannelStable
|| anyPrereleaseTagsExist; otherwise they keep calling GetLatestRelease
verbatim (zero behaviour change for the default tool). A new
UpdaterOption WithChannel(props.UpdateChannel) lets direct/test callers set it
parallel-safely (mirroring WithReleaseProvider).
Pinning¶
A pin durably fixes the installation to an exact version. It is persisted in
the tool config dir next to the existing last_checked / last_updated markers
(pkg/setup's GetDefaultConfigDir), in a new pinned_version marker file
(0600, owner-only β same markerFilePerm as the others):
// pkg/setup (new, alongside SetCheckedVersion / GetCheckedVersion)
func SetPinnedVersion(fs afero.Fs, name, version string) error // ""/"" clears
func GetPinnedVersion(fs afero.Fs, name string) string // "" when unpinned
Semantics:
update --pin(no value) β pin to the current running version.update --pin vX.Y.Zβ pin to that version (validated againstsemVerPattern; the update proceeds to install it if not already current β pin-and-move).update --unpinβ clear the pin.- While pinned, the background check (
checkForUpdates) emits at most a single INFO ("pinned to vX.Y.Z; run<tool> update --unpinto resume updates") and does not nag about newer releases or auto-update under any policy. An explicitupdate(no--version) while pinned is a no-op with a clear message unless--unpin/--version/--rollback/--forceoverrides it (OQ3 covers--forceprecedence). - A pin to a version on the user's channel ladder is the norm; pinning to a tag outside the channel is allowed (an explicit user act overrides the channel) and warns once.
This makes the "stay put" intent durable and machine-checkable, which the current
single-run --version vX.Y.Z does not.
Rollback¶
A rollback deliberately installs a release older than the running binary.
It reuses the entire existing download β checksum-verify β signature-verify β
extract β replace pipeline (SelfUpdater.Update via GetReleaseByTag); the only
differences are direction permission and user intent confirmation:
update --rollback(no value) β roll back to the immediately preceding release on the current channel (the second-highest channel candidate, or the preceding stable). Useful for "the release I just took is bad, get me off it".update --rollback vX.Y.Zβ roll back to an explicit prior tag.- The target must resolve via
GetReleaseByTagand pass the sameverifyAssetChecksum+ signature verification as a forward update β a downgrade is not a trust bypass.require_checksum/require_signatureare honoured identically. - Because
IsLatestVersion/shouldSkipUpdatetreat "target < current" as an anomaly, rollback sets an explicitallowDowngradeflag on theSelfUpdater(set by aWithAllowDowngrade()option, only ever set on the rollback path) soshouldSkipUpdatepermits the backward move instead of reporting "already on a newer version". - Interactive confirmation: a rollback prompts ("Roll back from vA to vB?
This installs an older release.") unless
--cior--forceis set (non-interactive rollback requires--forceto proceed, so a scripted downgrade is always explicit). - A successful rollback implicitly pins to the target (a rollback that the
next background check immediately "fixes" back to latest would be useless). The
pin is written as part of the rollback;
--unpinlater resumes normal updates. (This coupling is OQ1 β auto-pin-on-rollback vs. leave-unpinned-and-warn.)
Command surface¶
pkg/cmd/update.NewCmdUpdate gains flags, preserving the existing
--versionβ--from-file exclusivity and adding new exclusivity groups:
--channel string one-shot channel override for this invocation (stable|beta|canary)
--pin [version] pin to version (or current if omitted); persists
--unpin clear an existing pin
--rollback [version] roll back to a prior release (or previous on channel)
Exclusivity (MarkFlagsMutuallyExclusive): --from-file remains exclusive with
--version/--rollback/--channel; --pinβ--unpin; --rollbackβ--unpin.
--version and --rollback are exclusive (both name a target; rollback adds the
downgrade-confirm + auto-pin semantics). --channel composes with --version?
No β an explicit --version already names the target, so --channel is ignored
with a warning when both are given (OQ3).
Validation reuses semVerPattern for every version-bearing flag. Channel values
are validated by UpdateChannel.Valid() after Normalize().
Config surface¶
update:
policy: prompt # existing
check_interval: "" # existing
channel: stable # NEW: stable | beta | canary (empty = tool baseline)
update.channel flows through the same Viper precedence chain as every other
key (CLI flag > env > file > embedded > default); no new config mechanism. The
pin is not a config key β it is operational state (a marker file), like
last_checked, so it survives init re-runs and is per-installation rather than
per-config-file. (Putting the pin in config would make it spuriously
diff/commit-able and would fight the multi---config merge.)
Background-check integration (pkg/cmd/root)¶
checkForUpdates / shouldSkipUpdateCheck gain two reads, both cheap and local:
- If
GetPinnedVersionis non-empty β skip the network check entirely and emit the single pinned-INFO (still subject to--cisuppression). The persistent "you're behind" WARN (warnIfBehindCached) is suppressed while pinned β being behind is intentional. - Otherwise resolve
update.channeland pass it to the updater so the cached "latest" reflects the user's channel, not always-stable. The cached value continues to live in thelast_checkedmarker body (no new file for the common path).
The UpdatePolicy branching (enabled/prompt/disabled) is unchanged β channel and
pin sit upstream of it (they decide the candidate; policy decides what to do
about it).
Public API¶
pkg/props¶
type UpdateChannel string
const ( ChannelStable; ChannelBeta; ChannelCanary )
func (UpdateChannel) Normalize() UpdateChannel
func (UpdateChannel) Valid() bool
func (UpdateChannel) Includes(UpdateChannel) bool
func ChannelOf(tag string) UpdateChannel
func ResolveUpdateChannel(toolDefault UpdateChannel, configValue string) UpdateChannel
// Tool gains:
// UpdateChannel UpdateChannel `json:"update_channel,omitempty" yaml:"update_channel,omitempty"`
// Zero value normalises to ChannelStable.
pkg/setup¶
func WithChannel(ch props.UpdateChannel) UpdaterOption
func WithAllowDowngrade() UpdaterOption // rollback path only
func (s *SelfUpdater) GetLatestReleaseForChannel(ctx context.Context, ch props.UpdateChannel) (release.Release, error)
func SetPinnedVersion(fs afero.Fs, name, version string) error
func GetPinnedVersion(fs afero.Fs, name string) string
SelfUpdater gains unexported channel props.UpdateChannel and allowDowngrade
bool fields, resolved/wired in NewUpdater.
pkg/cmd/update¶
New flags --channel, --pin, --unpin, --rollback on NewCmdUpdate; new
exported UpdateResult fields:
type UpdateResult struct {
PreviousVersion string `json:"previous_version"`
NewVersion string `json:"new_version"`
Updated bool `json:"updated"`
Channel string `json:"channel,omitempty"` // NEW
Pinned bool `json:"pinned,omitempty"` // NEW
RolledBack bool `json:"rolled_back,omitempty"` // NEW
}
No change to the release.Provider, Release, ReleaseAsset,
ChecksumProvider, or SignatureProvider interfaces, nor to
ReleaseSourceConfig / props.ReleaseSource. This is deliberate β channels
are a client-side view over the unchanged release stream.
Error Handling¶
- Unknown
--channel/update.channelβ reject with a clear message listing the three valid values (validated viaUpdateChannel.Valid()). --rollback/--pin/--versionto a tag that does not exist β the existingrelease.ErrReleaseNotFound-wrapped surface fromGetReleaseByTag(no new error type; the injectable-release-source spec already introduced the sentinel).- Rollback where the target's checksum/signature fails verification β the existing verification errors abort the install, binary unchanged β a downgrade gets no weaker treatment. No new error types.
--rollbacknon-interactive without--force/--ciβ refuse with a non-zero exit and a message explaining a downgrade needs explicit confirmation (mirrors the ForcedUpdate "never mask a destructive action" principle).- A channel selection that finds no candidate on the ladder (e.g.
betaselected but only stable exists) falls back to the newest stable with an INFO ("no beta release found; latest stable is vX.Y.Z"), never an error. - All new errors use
cockroachdb/errorswith hints perdocs/development/error-handling.md.
Testing Strategy (TDD)¶
Per the project TDD workflow, failing tests first, derived from the contracts.
Unit (pkg/props)¶
ChannelOfmapping for stable /-beta.N/-canary.N/-rc.N/ malformed tags;Includesladder truth table;Normalize/Validedge cases;ResolveUpdateChannelprecedence (config > baseline > stable), case-insensitive, empty/invalid fallbacks β mirroring the existingResolveUpdatePolicytests.ToolwithUpdateChannelround-trips through JSON/YAML (and omitempty when unset). β₯90% coverage on the new file.
Unit (pkg/setup)¶
GetLatestReleaseForChannelover areleasetest.Sourceseeded with a mix of stable/beta/canary/draft tags: picks the correct max per channel; ladder inclusion; drafts skipped; empty-channel fallback to stable.SetPinnedVersion/GetPinnedVersionround-trip, clear, 0600 perms, empty-dir no-op (same invariants asSetCheckedVersion).- Rollback:
allowDowngradeletsshouldSkipUpdatepermit "target < current"; checksum/signature verification still runs on the downgrade target (assert a corrupt-checksum rollback aborts, binary unchanged β reuse thereleasetestcorrupt/bad-sig constructors); auto-pin written on success. WithChannel/WithAllowDowngradeare parallel-safe (two concurrent updaters, different channels, no interference) β the project's package-mock ban.
Unit (pkg/cmd/update, pkg/cmd/root)¶
- Flag parsing + exclusivity matrix (
--pinβ--unpin,--versionβ--rollback,--from-fileexclusivity preserved); invalid version/channel rejected. checkForUpdates: pinned β network check skipped, pinned-INFO emitted,warnIfBehindCachedsuppressed; unpinned β channel threaded into the updater.
E2E BDD (Godog, gated)¶
Per the Godog suitability assessment
(2026-03-28-godog-bdd-strategy.md),
channels/pin/rollback are multi-step, user-visible CLI workflows β BDD adds
value, and the injectable release source
(2026-06-19-injectable-release-source.md)
makes them hermetic. Extend cmd/e2e's GTB_E2E_RELEASE_SCENARIO selector with
channel-bearing fixtures (a releasetest.Source seeded with stable+beta+canary
tags) and add features/cli/update.feature scenarios that assert outcomes
that abort or no-op before any binary swap (safe against the shared E2E
binary):
--channel betaselects the newest beta over an older stable (assert the targeted version in output; happy apply stays Go-side per the injectable spec's self-replacement caveat);- pinned tool: background check reports "pinned", does not nag;
--rollbackto a tag with a corrupt checksum β non-zero exit, checksum message, binary unchanged;--rollbacknon-interactive without--forceβ refused, non-zero exit.
The happy forward/rollback apply path stays Go-side (update_e2e_test.go on
releasetest) β binary self-replacement is unsafe against the shared E2E binary
(same reasoning as the injectable-release-source spec).
Verification gates¶
just ci (tidy, generate, test, test-race, lint), just mocks clean, β₯90%
coverage on new pkg/ code, @cli/@generator Godog suites green.
Generator Impact¶
props.Toolis scaffolded byinternal/generator/templates/skeleton_root.go. The newUpdateChannelfield is additive +omitempty; existing scaffoldedprops.Tool{β¦}literals compile unchanged (nil/zero β stable). As with theUpdatePolicybaseline, the generator may expose an--update-channelflag + wizard step (validated by a newgenerator.ValidateUpdateChannel) renderingUpdateChannel: props.ChannelStableinto the literal β or leave it unset and rely on the zero-value default. OQ5 decides flag-vs-default; the low-friction default-only path is preferred unless review wants the wizard affordance.- Re-run the generator golden/AST tests and a
generate project -p tmp && go build ./...smoke to confirm the scaffolded tree builds against the updatedpkg/props.
Migration & Compatibility¶
- Additive, backwards compatible. No channel + no pin = today's stable,
forward-only behaviour exactly. No interface,
ReleaseSource, or wire-format change. - Pre-1.0 (per CLAUDE.md): adding an exported
propstype, aToolfield, twoUpdaterOptions, andpkg/setuphelpers is non-breaking;just apidiffshows them as advisory additions. A one-linedocs/migration/entry records the new channel/pin/rollback surface for the v1.0 guide. - Docs:
docs/components/update/release pages gain channel/pin/rollback sections, including the tagging convention a tool author must follow to publish a channel (vX.Y.Z-beta.N/-canary.N), and thedocs/development/feature-decisions.md"channels β profiles" note.
Future Considerations¶
- Server-driven / staged rollout (percentage cohorts, remote channel assignment) β explicitly out of scope; would need release-side infrastructure GTB does not own.
- Automatic rollback-on-failure (keep the previous binary and auto-revert if the new one crashes on first run) β a larger feature (previous-binary cache, health gate) tracked separately.
rcas a first-class fourth channel between beta and stable, rather than folding-rc.*into beta (OQ2).- Paging
ListReleasesbeyond one page to find deep historical rollback targets (OQ4). - Aligning a provider-native prerelease signal if a future need outweighs the cross-backend interface churn (today the semver label is authoritative).
Open Questions¶
- Auto-pin on rollback β does a successful
--rollbackimplicitly pin (spec's assumption, so the next check doesn't immediately re-upgrade), or stay unpinned and rely on a loud WARN each run? Auto-pin is safer and matches user intent ("get me off the bad release and keep me off it"); the cost is a user must--unpinto resume. Lean: auto-pin. -rc.*mapping β fold release candidates intobeta(spec's default), or add a distinctrcchannel between beta and stable? Folding keeps three channels (simpler); a fourth is more precise for tools that publish RCs.- Flag precedence/composition β
--channel+--version(spec:--versionwins,--channelignored with a warn);--force+--pin(does--forceoverride a pin for a one-shot update without clearing it?);--rollback+--force(force = skip the interactive downgrade confirm). Confirm the matrix. ListReleasessingle-page bound β rollback/channel selection scans the newestreleasesPerPage(100) releases. Acceptable for the foreseeable range; a tool with >100 releases since the rollback target can't reach it without--version. Document as a known limit, or page now? Lean: document.- Generator surface β expose
--update-channel+ wizard step (consistent with--update-check-interval), or rely on the zero-value stable default only? Lean: default-only, add the flag only if review wants the affordance. - Channel of the running binary β for the "are you behind?" comparison on a non-stable channel, do we compare against the channel-max (so a beta user is "behind" only when a newer beta/stable exists)? Spec assumes yes (the channel-aware candidate is the comparison basis). Confirm this is the intended UX and doesn't surprise a user who hand-installed a beta.
Roadmap item A6. DRAFT β pending human review of the open questions above before implementation, per the project's spec-driven workflow.
Resolutions (open questions confirmed with user 2026-06-21)¶
- Auto-pin on rollback β RESOLVED: yes, auto-pin. A successful
--rollbackpins the rolled-back version so the next check doesn't re-upgrade into the bad release; the user runs--unpinto resume. -rc.*mapping β RESOLVED: fold RCs intobeta. Keep three channels (stable/beta/canary); release candidates map to beta.- Flag precedence/composition β RESOLVED: confirm the proposed matrix β
--versionbeats--channel(channel ignored, with a warn);--forceoverrides a pin for a one-shot update without clearing the pin;--rollback --forceskips the interactive downgrade confirmation. ListReleasespage bound β RESOLVED: document the 100-release scan as a known limit; older rollback targets require explicit--version. No pagination in v1.- Generator surface β RESOLVED: default-only (stable); no
--update-channelflag or wizard step in v1. Add the affordance later only if wanted. - "Behind?" basis on a non-stable channel β RESOLVED (no user preference β adopt the spec assumption): compare against the channel-max. A beta user is "behind" only when a newer beta-or-stable exists; the channel-aware candidate is the comparison basis.