Help us shape the future of Go! Take the 2026 Go Developer Survey

plan

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package plan is the plan/confirm screen: it runs discovery + digest resolution + gitops.BuildPlan for one source/target env pair, shows a tickable list of image repos on the left and, on the right, what the promotion ships for the repo under the cursor — the commits between what the target declares and what the source resolved to, and the migrations among them (M10, #85 screen 04/12) — with the unified diff one key away. rows.go derives the rows, the diff and the warning text from a gitops.Plan and resolve.Resolution map with no terminal dependency, so it is unit-testable as plain values (AGENTS.md §4.8); model.go lays that data out with huh fields (MultiSelect for the repo list, Select for the target-env prompt, Confirm for the direct-mode switch) and a bubbles/v2 spinner + viewport, inside the M10 frame.

Index

Constants

View Source
const (
	ModePR     = writepath.ModePR
	ModeDirect = writepath.ModeDirect
)

Mode is the write path this plan would use.

Variables

This section is empty.

Functions

func CommonPrefix

func CommonPrefix(rows []Row) string

CommonPrefix is the image-repo prefix every row shares, up to and including its last "/", so the repo names on the rows can drop it and never wrap mid-token (#85: the version you are approving was being split across lines at an arbitrary column). "" when the rows share nothing, or there is only one row (no prefix is shorter than the whole name).

func ImageRefStats added in v0.2.0

func ImageRefStats(pl gitops.Plan, ticked map[string]bool) (refs, files int)

ImageRefStats counts the image references this plan would actually write for the ticked repo set, and the distinct files they touch — the totals line's own "N image references, M files" (mirroring deploy.scale's identical count). A no-op edit writes nothing (Edit.NoOp()) and is excluded, exactly as RenderDiff already skips it into no diff hunk.

func Labels added in v0.2.0

func Labels(rows []Row, prefix string, width int) []string

Labels is one ShortLabel per row, made distinct where truncation would otherwise leave two rows reading the same (#121: two repos whose names differ only past the pane's width, with the same versions, were indistinguishable on the left — and the left row is the tick target). A colliding group drops the prefix its own members share and shows the rest behind "…", so the first visible character is the one that differs; anything still colliding after that (two such groups with the same remainders) is numbered by row position. Rows that never collided keep their plain ShortLabel.

func SkippedStaging

func SkippedStaging(source, target string, envs config.EnvsConfig) (staging string, skip bool)

SkippedStaging reports the configured staging env for source when target is production but is not that configured pair — the "deploying straight to production, skipping <staging>" warning (AGENTS.md §4.5). It never blocks (principle 5); skip is false when target is not production, or is exactly the configured pair, or source has no configured pair to skip.

func SourcesFor added in v0.2.0

func SourcesFor(r *gitops.Repo, target string) []string

SourcesFor lists the candidate source envs for target: every other discovered env, sorted. It backs the huh.Select shown when p (matrix.OpenPlanMsg) named a target with no unambiguous reverse pair — the mirror of TargetsFor, above, which is still used where a source is known but has no configured target of its own.

func Summary

func Summary(o service.Resolution) []string

Summary is the resolution section: which cluster context and registry credential source were consulted (by name only, AGENTS.md §4.4), then each resolved repo's source and detail, in repo order. It renders the same facts cmd/hoist's printResolution does.

func TargetsFor

func TargetsFor(r *gitops.Repo, source string) []string

TargetsFor lists the candidate target envs for source: every other discovered env, sorted. It backs the huh.Select shown when the current env has no configured pair.

func ValidateOverride added in v0.2.0

func ValidateOverride(repo, text string) (image.Ref, error)

ValidateOverride is what the o dialog applies to the text on enter: image.ParseOverride — the one predicate `--digest` applies too (AGENTS.md §8, layered checks: the CLI and the TUI must refuse the same inputs with the same words) — plus the dialog's own rule that the override is for the row it was opened on, since the dialog pre-fills that repo and a plan built with an override for some other repo would not be the one the operator was looking at. The returned Ref is what the resolver is handed.

func WarningRepo

func WarningRepo(w gitops.Warning) string

WarningRepo is the repo a warning is about: every Warning built by pkg/gitops and pkg/resolve carries Occurrences for exactly one repo (never a mix), so the first occurrence's ref names it; "" when a warning carries no occurrence at all. Exported so internal/app's filterTicked (app.go) can correlate a Plan's Warnings against the operator's ticked selection the same way DeriveRows already groups them, rather than re-deriving the "first occurrence's ref" convention a second time.

Types

type BackMsg

type BackMsg struct{}

BackMsg is emitted when the screen wants the root to pop back to whatever was underneath it. internal/app/doc.go notes that pop arrives with the first screen pushed on top of the matrix; this is that screen, so the root recognizes BackMsg by its concrete type in its own Update switch — screens still never import app (AGENTS.md §4.8).

type Func added in v0.2.0

Func builds one gitops.Plan through internal/service's own Plan — the identical builder `hoist plan`/`hoist promote`/`hoist deploy` use, so a plan built for this screen and one built for the CLI's dry run can never silently diverge again. cmd/hoist supplies it as svc.Plan, wrapping whichever cluster and registry adaptors the selected repo's config calls for — so this package never opens a cluster or registry connection itself (AGENTS.md §4.3) and never imports cmd (AGENTS.md §4.8). It always talks to a cluster/registry when resolution is configured, so model.go calls it only from inside a tea.Cmd, never from Update directly. req.Overrides are the operator's per-repo digest overrides (the o dialog, #102 — the TUI's `--digest`): Plan hands them to pkg/resolve exactly as the CLI does, so each one wins outright and is reported as [override] with the same alternatives and warnings.

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model is the plan screen. It is a value: Update, SetSize and SetStyles return the updated model, matching internal/app/matrix's convention.

func New

func New(repo *gitops.Repo, promotable []string, envs config.EnvsConfig, source, target string, planFn Func, hist history.Funcs) Model

New builds the plan screen, which retires the old "P forces a prompt" gesture: p on the matrix always names a Target (the cursor's column), and Source is either the one unambiguous reverse pair or "" — in which case this screen prompts "promote into <target> from…" itself, rather than the matrix ever forcing a prompt for an arbitrary target. A caller with a source but no target (kept for a caller that still has one, e.g. a future non-matrix producer) still gets the older "promote <source> to…" prompt instead. planFn is nil in tests that never load a plan through the real service (a fixture builds gitops.Plan directly via loadedMsg instead). hist is the commit-history bundle (M10); a zero value degrades every repo to a named gap.

func (Model) CapturesText

func (m Model) CapturesText() bool

CapturesText implements app.Screen (via planScreen's thin delegate in internal/app/screen.go). The root queries this before treating "q" as its own global quit key. huh.Select and huh.MultiSelect both support their own "/" filter-typing mode (GetFiltering), but that mode — like Down/Up/Space navigation generally — is only reachable once huh.Field.WithKeyMap has been called on the field. That normally happens automatically inside a huh.Form/Group; this screen uses both fields standalone (AGENTS.md §4.7, "no layout library", ruled out adopting huh.Form just for its wiring), so buildEnvSelect and buildMultiSelect call WithKeyMap directly at construction time instead — plain component wiring, not a layout dependency. With that in place, query whichever field is actually live for its own real filtering state: the env-select prompt while it's up, or the multiSelect while it holds focus and no huh.Confirm dialog is covering it.

func (Model) Close added in v0.2.0

func (m Model) Close()

Close cancels this screen's outstanding history and load requests. Called by the root's own pop/truncate the moment this screen is actually removed from the stack (AGENTS.md §4.8) — never by this package itself any more: Enter hands off to the building screen without removing this one (m.start's own design), and cancelling here on Enter used to strand every not-yet-loaded delta if that build then failed and the building screen popped back to a plan screen whose history could never load again (FB-M3).

func (Model) Init

func (m Model) Init() tea.Cmd

Init kicks off whatever the starting state needs: the env-select prompt's focus, or the spinner tick plus the async load. Resolution talks to a cluster/registry, so it is a tea.Cmd here, never run inside Update.

func (Model) KeyScreen added in v0.2.0

func (m Model) KeyScreen() keys.Screen

KeyScreen implements the root's keyed interface (internal/app/screen.go): the help overlay and the activity log ("l") only reach this screen once it names its own registry row.

func (Model) Reload added in v0.2.0

func (m Model) Reload(repo *gitops.Repo) (Model, tea.Cmd)

Reload is RefreshMsg's own rebuild: the root calls this once matrix.RepoRefreshedMsg lands with a fresh *gitops.Repo, from origin, so this screen's plan is rebuilt against exactly what F5 just fetched rather than the boot-time snapshot it was opened with. A no-op outside stateReady: a load already in flight (stateLoading) or a still-open env prompt has nothing yet to rebuild FROM. keepTicked/a fresh scope ID mirror the override rebuild's own shape (updateOverride) — the operator's ticked set survives, narrowed to the fresh plan's selectable rows, with any row that was not selectable before ticked as a first load would tick it (withNewlySelectable, #161: a repo that appears after the refresh arrives ticked), and a load already outstanding for the stale repo is dropped by the scope.Foreign guard rather than landing on rows this reload never asked for.

func (Model) ResetStarting added in v0.2.0

func (m Model) ResetStarting() Model

ResetStarting clears the one-shot Enter guard starting sets — see its own doc comment. The root calls this after popping a failed build back onto this same screen instance.

func (Model) SetSize

func (m Model) SetSize(width, height int) Model

SetSize lays every huh field and the viewport out inside width × height.

func (Model) SetStyles

func (m Model) SetStyles(s ui.Styles) Model

SetStyles applies the palette (and its dark/light flag to every huh field via huh's own Charm theme — AGENTS.md §4.7: no layout library, but a component's own theming is not one).

func (Model) Update

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)

Update handles the screen's own keys, the loading messages, and forwards everything else to whichever huh field or bubbles component owns the current state.

func (Model) View

func (m Model) View() string

View renders the current state. Every rendered string passes through redact.Strings once more here, at the output boundary, in addition to each render point that already calls it (Summary's per-repo Detail, the disabled-row Reason, warning messages, viewReady's own fatal-error line) — so a display field that forgets to redact itself, or a credential registered after an earlier call already built its string, is still caught before it reaches the terminal (AGENTS.md §4.4/§4.10, R-002).

func (Model) WithNow

func (m Model) WithNow(now func() time.Time) Model

WithNow fixes the clock relative dates are worded against (tests).

type RefreshMsg added in v0.2.0

type RefreshMsg struct{}

RefreshMsg is r/F5/ctrl+r: rebuild the plan at fresh origin. The screen has no way to fetch origin itself (AGENTS.md §4.3 — a screen never opens a git/cluster/registry connection; service.Plan itself never fetches either, it only reads whatever repo the service currently holds), so this asks the root to run the matrix's own completion-triggered refresh (requestMatrixRefresh, the same fetch F5 already runs there) and rebuild this screen's plan once matrix.RepoRefreshedMsg lands with the new *gitops.Repo — internal/app/app.go's own new case, mirroring deploy.RefreshMsg exactly (the screen's own shared "r" path).

type Row

type Row struct {
	Repo string
	// Old and New are the references shown as "old-tag → new-tag"; Old is the first target
	// occurrence's existing ref (occurrences of one repo in one env always share a tag in
	// practice, and a disagreement is its own warning), New is what the plan would write.
	Old, New image.Ref
	// Count is the number of target-env occurrences this repo's edits touch.
	Count int
	// Source names where the digest came from: a resolve.Source ("pods", "manifest",
	// "registry", "override"), "manifest" when no resolution ran at all (digest sources:
	// none, exactly as M1 planned), or "" only for a disabled row.
	Source string
	// Warnings are every warning (resolution or plan) attached to this repo; a non-empty
	// list draws the "!" marker on the row.
	Warnings []gitops.Warning
	// Disabled is true when pkg/resolve could not supply a digest for this repo at all
	// (WarnUnresolved): the row is shown but never offered as a tickable option, since huh
	// has no per-option disable and the row's own reference may still be a stale manifest
	// pin no one has confirmed against the running env.
	Disabled bool
	// Reason explains Disabled; "" otherwise.
	Reason string
	// Files is the number of distinct target-env files this repo's edits touch.
	Files int
	// NoOp is true when every edit for this repo already matches the target (Edit.NoOp()):
	// promoting this repo would write nothing. Never true alongside Disabled — an unresolved
	// row has no confirmed New reference to compare against, so resolution failure is checked
	// first (UX-M17: "no-op rows shown greyed").
	NoOp bool
}

Row is one image repo in the left pane, derived from a built Plan and (when resolution ran) its Resolutions. It carries no terminal dependency so it is unit-testable as plain values (AGENTS.md §4.8); model.go lays it out and model.go alone talks to huh.

func DeriveRows

func DeriveRows(pl gitops.Plan, res map[string]resolve.Resolution) []Row

DeriveRows groups pl.Edits by image repo into one Row each, sorted by repo. res is the resolve.Resolve output for the source env; a nil or empty map means no resolution ran (digest sources: none) and every row's Source reads "manifest", matching what BuildPlan alone would have written in M1.

func Disabled

func Disabled(rows []Row) []Row

Disabled is the rows shown without a checkbox, each with its reason.

func NoOps added in v0.2.0

func NoOps(rows []Row) []Row

NoOps is the rows already current in the target env — shown greyed with the reason, never tickable (UX-M17: "no-op rows shown greyed", the plan-confirm mockup's own "· worker · already current").

func Selectable

func Selectable(rows []Row) []Row

Selectable is the rows huh.MultiSelect offers a checkbox for — neither unresolved (Disabled) nor already current (NoOp): ticking a no-op row would promote nothing, so it is shown but never offered as a tickable option, the same convention Disabled already uses for a different reason (UX-M17).

func (Row) Label

func (r Row) Label() string

Label is the row text: "repo old → new (n occurrences) [source]", with a leading "!" when the row carries warnings.

func (Row) ShortLabel

func (r Row) ShortLabel(prefix string, width int) string

ShortLabel is the row text for the left pane (M10): the repo without the prefix every row shares (CommonPrefix, shown once in the header), the versions, and "!" when the row carries warnings — short enough that the version never wraps mid-token at the pane's width; the occurrence count and source moved to the right pane's own head line.

type StartMsg

type StartMsg struct {
	Plan    gitops.Plan
	Outcome service.Resolution
	Mode    string
	// Ticked is the repo set the operator selected in the multiSelect, unmodified — the
	// same set recomputeDiff already filters Plan.Edits by.
	Ticked         []string
	Source, Target string
	// View is the service.RepoView the underlying planFn call actually planned against
	// (service.PlannedChange.View, carried through loadedMsg) — the root's StartPromotion call
	// passes this as StartRequest.View so the freshness check re-checks the SAME view this plan
	// was built from, rather than whatever the service's current view has since become (an F5
	// refresh between loading this screen and pressing Enter must not silently launder a plan
	// built from a now-stale view).
	View service.RepoView
}

StartMsg is emitted when the operator confirms this plan (Enter, in stateReady) — whichever ticked repos are selected should now start driving as a promotion. It carries plan-shaped data only: this package has no repoFullName (RepoConfig.GitHub), no CI/ approval policy, and no git.Git/forge.Forge adaptor to build a real engine.PromotionState or a flight.DriveFunc from (AGENTS.md §4.3/§4.8 — a screen never imports those adaptor packages). The root recognizes StartMsg by concrete type (AGENTS.md §4.8) and pushes internal/app/flight.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL